00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033 import cmd
00034 import threading
00035 import time
00036 import Queue
00037
00038 from miro import app
00039 from miro import dialogs
00040 from miro import eventloop
00041 from miro import folder
00042 from miro import indexes
00043 from miro import util
00044 from miro import views
00045 from miro.frontends.cli import clidialog
00046 from miro.plat import resources
00047 from miro import fileutil
00048 from miro import feed
00049
00050
00051 import os, sys, subprocess, re, fnmatch, string
00052 import logging
00053 from miro.singleclick import parse_command_line_args
00054 from miro import moviedata
00055 from miro import autodler
00056 from miro import downloader
00057 from miro import iconcache
00058 from miro.clock import clock
00059 from miro import filetypes
00060
00061
00062 def run_in_event_loop(func):
00063 def decorated(*args, **kwargs):
00064 return_hack = []
00065 event = threading.Event()
00066 def runThenSet():
00067 try:
00068 return_hack.append(func(*args, **kwargs))
00069 finally:
00070 event.set()
00071 eventloop.addUrgentCall(runThenSet, 'run in event loop')
00072 event.wait()
00073 if return_hack:
00074 return return_hack[0]
00075 decorated.__doc__ = func.__doc__
00076 return decorated
00077
00078 class FakeTab:
00079 def __init__(self, tab_type, tabTemplateBase):
00080 self.type = tab_type
00081 self.tabTemplateBase = tabTemplateBase
00082
00083 class MiroInterpreter(cmd.Cmd):
00084 def __init__(self):
00085 cmd.Cmd.__init__(self)
00086 self.quit_flag = False
00087 self.tab = None
00088 self.init_database_objects()
00089
00090 @run_in_event_loop
00091 def init_database_objects(self):
00092 self.channelTabs = util.getSingletonDDBObject(views.channelTabOrder)
00093 self.playlistTabs = util.getSingletonDDBObject(views.playlistTabOrder)
00094 self.tab_changed()
00095
00096 def tab_changed(self):
00097 """Calculate the current prompt. This method access database objects,
00098 so it should only be called from the backend event loop
00099 """
00100 if self.tab is None:
00101 self.prompt = "> "
00102 self.selection_type = None
00103 elif self.tab.type == 'feed':
00104 if isinstance(self.tab.obj, folder.ChannelFolder):
00105 self.prompt = "channel folder: %s > " % self.tab.obj.get_title()
00106 self.selection_type = 'channel-folder'
00107 else:
00108 self.prompt = "channel: %s > " % self.tab.obj.get_title()
00109 self.selection_type = 'feed'
00110 elif self.tab.type == 'playlist':
00111 self.prompt = "playlist: %s > " % self.tab.obj.get_title()
00112 self.selection_type = 'playlist'
00113 elif (self.tab.type == 'statictab' and
00114 self.tab.tabTemplateBase == 'downloadtab'):
00115 self.prompt = "downloads > "
00116 self.selection_type = 'downloads'
00117 else:
00118 raise ValueError("Unknown tab type")
00119
00120 def postcmd(self, stop, line):
00121
00122
00123
00124 time.sleep(0.1)
00125 while True:
00126 try:
00127 dialog = app.cli_events.dialog_queue.get_nowait()
00128 except Queue.Empty:
00129 break
00130 clidialog.handle_dialog(dialog)
00131
00132 return self.quit_flag
00133
00134 def do_help(self, line):
00135 """help -- Lists commands and help."""
00136 commands = [m for m in dir(self) if m.startswith("do_")]
00137 for mem in commands:
00138 docstring = getattr(self, mem).__doc__
00139 print " ", docstring
00140
00141 def do_quit(self, line):
00142 """quit -- Quits Miro cli."""
00143 self.quit_flag = True
00144
00145 @run_in_event_loop
00146 def do_feed(self, line):
00147 """feed <name> -- Selects a feed by name."""
00148 for tab in self.channelTabs.getView():
00149 if tab.obj.get_title() == line:
00150 self.tab = tab
00151 self.tab_changed()
00152 return
00153 print "Error: %s not found" % line
00154
00155 @run_in_event_loop
00156 def do_rmfeed(self, line):
00157 """rmfeed <name> -- Deletes a feed."""
00158 for tab in self.channelTabs.getView():
00159 if tab.obj.get_title() == line:
00160 tab.obj.remove()
00161 return
00162 print "Error: %s not found" % line
00163
00164 @run_in_event_loop
00165 def complete_feed(self, text, line, begidx, endidx):
00166 return self.handle_tab_complete(text, self.channelTabs.getView())
00167
00168 @run_in_event_loop
00169 def complete_rmfeed(self, text, line, begidx, endidx):
00170 return self.handle_tab_complete(text, self.channelTabs.getView())
00171
00172 @run_in_event_loop
00173 def complete_playlist(self, text, line, begidx, endidx):
00174 return self.handle_tab_complete(text, self.playlistTabs.getView())
00175
00176 def handle_tab_complete(self, text, view):
00177 text = text.lower()
00178 matches = []
00179 for tab in view:
00180 if tab.obj.get_title().lower().startswith(text):
00181 matches.append(tab.obj.get_title())
00182 return matches
00183
00184 def handle_item_complete(self, text, view, filterFunc=lambda i: True):
00185 text = text.lower()
00186 matches = []
00187 for item in view:
00188 if (item.get_title().lower().startswith(text) and
00189 filterFunc(item)):
00190 matches.append(item.get_title())
00191 return matches
00192
00193
00194
00195
00196
00197
00198
00199 @run_in_event_loop
00200 def do_mythtv_update_autodownload(self, line):
00201 """Update feeds and auto-download"""
00202 logging.info("Starting auto downloader...")
00203 autodler.start_downloader()
00204 feed.expire_items()
00205 starttime = clock()
00206 logging.timing("Icon clear: %.3f", clock() - starttime)
00207 logging.info("Starting video updates")
00208 moviedata.movieDataUpdater.startThread()
00209 parse_command_line_args()
00210
00211
00212
00213 eventloop.addTimeout(5, downloader.startupDownloader,
00214 "start downloader daemon")
00215
00216 eventloop.addTimeout(30, feed.start_updates, "start feed updates")
00217
00218
00219 eventloop.addTimeout(10, iconcache.clear_orphans, "clear orphans")
00220
00221 def movie_data_program_info(self, movie_path, thumbnail_path):
00222 extractor_path = os.path.join(os.path.split(__file__)[0], "gst_extractor.py")
00223 return ((sys.executable, extractor_path, movie_path, thumbnail_path), None)
00224
00225 @run_in_event_loop
00226 def do_mythtv_check_downloading(self, line):
00227 """Check if any items are being downloaded. Set True or False"""
00228 self.downloading = False
00229 downloadingItems = views.downloadingItems
00230 count = len(downloadingItems)
00231 for item in downloadingItems:
00232 logging.info(u"(%s - %s) video is downloading with (%0.0f%%) complete" % (item.get_channel_title(True).replace(u'/',u'-'), item.get_title().replace(u'/',u'-'), item.download_progress()))
00233 if not count:
00234 logging.info(u"No items downloading")
00235 if count:
00236 self.downloading = True
00237
00238 @run_in_event_loop
00239 def do_mythtv_updatewatched(self, line):
00240 """Process MythTV update watched videos"""
00241 items = views.watchableItems
00242 for video in self.videofiles:
00243 for item in items:
00244 if item.get_filename() == video:
00245 break
00246 else:
00247 logging.info(u"Item for Miro video (%s) not found, skipping" % video)
00248 continue
00249 if self.simulation:
00250 logging.info(u"Simulation: Item (%s - %s) marked as seen and watched" % (item.get_channel_title(True), item.get_title()))
00251 else:
00252 item.markItemSeen(markOtherItems=False)
00253 self.statistics[u'Miro_marked_watch_seen']+=1
00254 logging.info(u"Item (%s - %s) marked as seen and watched" % (item.get_channel_title(True), item.get_title()))
00255
00256 @run_in_event_loop
00257 def do_mythtv_getunwatched(self, line):
00258 """Process MythTV get all un-watched video details"""
00259 if self.verbose:
00260 print
00261 print u"Getting details on un-watched Miro videos"
00262
00263 self.videofiles = []
00264 if len(views.watchableItems):
00265 if self.verbose:
00266 print u"%-20s %-10s %s" % (u"State", u"Size", u"Name")
00267 print u"-" * 70
00268 for item in views.watchableItems:
00269
00270 if not item.isVideo:
00271 continue
00272 state = item.get_state()
00273 if not state == u'newly-downloaded':
00274 continue
00275
00276 if filetypes.is_torrent_filename(item.getURL()):
00277 continue
00278 self.printItems(item)
00279 self.videofiles.append(self._get_item_dict(item))
00280 if self.verbose:
00281 print
00282 if not len(self.videofiles):
00283 logging.info(u"No un-watched Miro videos")
00284
00285 @run_in_event_loop
00286 def do_mythtv_getwatched(self, line):
00287 """Process MythTV get all watched/saved video details"""
00288 if self.verbose:
00289 print
00290 print u"Getting details on watched/saved Miro videos"
00291 self.videofiles = []
00292 if len(views.watchableItems):
00293 if self.verbose:
00294 print u"%-20s %-10s %s" % (u"State", u"Size", u"Name")
00295 print "-" * 70
00296 for item in views.watchableItems:
00297
00298 if not item.isVideo:
00299 continue
00300 state = item.get_state()
00301 if state == u'newly-downloaded':
00302 continue
00303
00304 if filetypes.is_torrent_filename(item.getURL()):
00305 continue
00306 self.printItems(item)
00307 self.videofiles.append(self._get_item_dict(item))
00308 if self.verbose:
00309 print
00310 if not len(self.videofiles):
00311 logging.info(u"No watched/saved Miro videos")
00312
00313 def printItems(self, item):
00314 if not self.verbose:
00315 return
00316 state = item.get_state()
00317 if state == u'downloading':
00318 state += u' (%0.0f%%)' % item.download_progress()
00319 print u"%-20s %-10s %s" % (state, item.get_size_for_display(),
00320 item.get_title())
00321
00322
00323 @run_in_event_loop
00324 def do_mythtv_item_remove(self, args):
00325 """Removes an item from Miro by file name or Channel and title"""
00326 for item in views.watchableItems:
00327 if isinstance(args, list):
00328 if filter(self.is_not_punct_char, item.get_channel_title(True).lower()) == filter(self.is_not_punct_char, args[0].lower()) and (filter(self.is_not_punct_char, item.get_title().lower())).startswith(filter(self.is_not_punct_char, args[1].lower())):
00329 break
00330 elif filter(self.is_not_punct_char, item.get_filename().lower()) == filter(self.is_not_punct_char, args.lower()):
00331 break
00332 else:
00333 logging.info(u"No item named %s" % args)
00334 return
00335 if item.is_downloaded():
00336 if self.simulation:
00337 logging.info(u"Simulation: Item (%s - %s) has been removed from Miro" % (item.get_channel_title(True), item.get_title()))
00338 else:
00339 item.expire()
00340 self.statistics[u'Miro_videos_deleted']+=1
00341 logging.info(u'%s has been removed from Miro' % item.get_title())
00342 else:
00343 logging.info(u'%s is not downloaded' % item.get_title())
00344
00345
00346 def _get_item_dict(self, item):
00347 """Take an item and convert all elements into a dictionary
00348 return a dictionary of item elements
00349 """
00350 def compatibleGraphics(filename):
00351 if filename:
00352 (dirName, fileName) = os.path.split(filename)
00353 (fileBaseName, fileExtension)=os.path.splitext(fileName)
00354 if not fileExtension[1:] in [u"png", u"jpg", u"bmp", u"gif"]:
00355 return u''
00356 else:
00357 return filename
00358 else:
00359 return u''
00360
00361 def useImageMagick(screenshot):
00362 """ Using ImageMagick's utility 'identify'. Decide whether the screen shot is worth using.
00363 >>> useImageMagick('identify screenshot.jpg')
00364 >>> Example returned information "rose.jpg JPEG 640x480 DirectClass 87kb 0.050u 0:01"
00365 >>> u'' if the screenshot quality is too low
00366 >>> screenshot if the quality is good enough to use
00367 """
00368 if not self.imagemagick:
00369 return u''
00370
00371 width_height = re.compile(u'''^(.+?)[ ]\[?([0-9]+)x([0-9]+)[^\\/]*$''', re.UNICODE)
00372 p = subprocess.Popen(u'identify "%s"' % (screenshot), shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
00373
00374 response = p.stdout.readline()
00375 if response:
00376 match = width_height.match(response)
00377 if match:
00378 dummy, width, height = match.groups()
00379 width, height = int(width), int(height)
00380 if width >= 320:
00381 return screenshot
00382 return u''
00383 else:
00384 return u''
00385 return screenshot
00386
00387
00388 item_icon_filename = None
00389 channel_icon = None
00390 if item.getFeed():
00391 channel_icon = fileutil.expand_filename(item.getFeed().iconCache.get_filename())
00392
00393 if item.iconCache and item.iconCache.filename:
00394 item_icon_filename = item.iconCache.filename
00395
00396
00397 maximum_length = 128
00398 channel_title = item.get_channel_title(True).replace(u'/',u'-')
00399 if channel_title:
00400 if len(channel_title) > maximum_length:
00401 channel_title = channel_title[:maximum_length]
00402 channel_title = channel_title.replace(u'"', u'')
00403 title = item.get_title().replace(u'/',u'-')
00404 if title:
00405 if len(title) > maximum_length:
00406 title = title[:maximum_length]
00407 title = title.replace(u'"', u'')
00408 title = title.replace(u"'", u'')
00409
00410 item_dict = {u'feed_id': item.feed_id, u'parent_id': item.parent_id, u'isContainerItem': item.isContainerItem, u'isVideo': item.isVideo, u'seen': item.seen, u'autoDownloaded': item.autoDownloaded, u'pendingManualDL': item.pendingManualDL, u'downloadedTime': item.downloadedTime, u'watchedTime': item.watchedTime, u'pendingReason': item.pendingReason, u'title': title, u'expired': item.expired, u'keep': item.keep, u'videoFilename': item.get_filename(), u'eligibleForAutoDownload': item.eligibleForAutoDownload, u'duration': item.duration, u'screenshot': item.screenshot, u'resized_screenshots': item.resized_screenshots, u'resumeTime': item.resumeTime, u'channelTitle': channel_title, u'description': item.get_description(), u'size': item._get_size(), u'releasedate': item.get_release_date_obj(), u'length': item.get_duration_value(), u'channel_icon': channel_icon, u'item_icon': item_icon_filename, u'inetref': u'', u'season': 1, u'episode': 1,}
00411
00412 if not item_dict[u'screenshot']:
00413 if item_dict[u'item_icon']:
00414 item_dict[u'screenshot'] = useImageMagick(item_dict[u'item_icon'])
00415
00416 for key in [u'screenshot', u'channel_icon', u'item_icon']:
00417 if item_dict[key]:
00418 item_dict[key] = compatibleGraphics(item_dict[key])
00419
00420
00421
00422
00423
00424
00425
00426 return item_dict
00427
00428
00429
00430 def is_punct_char(self, char):
00431 '''check if char is punctuation char
00432 return True if char is punctuation
00433 return False if char is not punctuation
00434 '''
00435 return char in string.punctuation
00436
00437 def is_not_punct_char(self, char):
00438 '''check if char is not punctuation char
00439 return True if char is not punctuation
00440 return False if chaar is punctuation
00441 '''
00442 return not self.is_punct_char(char)
00443
00444
00445
00446
00447
00448
00449
00450 @run_in_event_loop
00451 def do_feeds(self, line):
00452 """feeds -- Lists all feeds."""
00453 current_folder = None
00454 for tab in self.channelTabs.getView():
00455 if isinstance(tab.obj, folder.ChannelFolder):
00456 current_folder = tab.obj
00457 elif tab.obj.getFolder() is not current_folder:
00458 current_folder = None
00459 if current_folder is None:
00460 print tab.obj.get_title()
00461 elif current_folder is tab.obj:
00462 print "[Folder] %s" % tab.obj.get_title()
00463 else:
00464 print " - %s" % tab.obj.get_title()
00465
00466 @run_in_event_loop
00467 def do_play(self, line):
00468 """play <name> -- Plays an item by name in an external player."""
00469 if self.selection_type is None:
00470 print "Error: No feed/playlist selected"
00471 return
00472 item = self._find_item(line)
00473 if item is None:
00474 print "No item named %r" % line
00475 return
00476 if item.is_downloaded():
00477 resources.open_file(item.get_video_filename())
00478 else:
00479 print '%s is not downloaded' % item.get_title()
00480
00481 @run_in_event_loop
00482 def do_playlists(self, line):
00483 """playlists -- Lists all playlists."""
00484 for tab in self.playlistTabs.getView():
00485 print tab.obj.get_title()
00486
00487 @run_in_event_loop
00488 def do_playlist(self, line):
00489 """playlist <name> -- Selects a playlist."""
00490 for tab in self.playlistTabs.getView():
00491 if tab.obj.get_title() == line:
00492 self.tab = tab
00493 self.tab_changed()
00494 return
00495 print "Error: %s not found" % line
00496
00497 @run_in_event_loop
00498 def do_items(self, line):
00499 """items -- Lists the items in the feed/playlist/tab selected."""
00500 if self.selection_type is None:
00501 print "Error: No tab/feed/playlist selected"
00502 return
00503 elif self.selection_type == 'feed':
00504 feed = self.tab.obj
00505 view = feed.items.sort(feed.itemSort.sort)
00506 self.printout_item_list(view)
00507 view.unlink()
00508 elif self.selection_type == 'playlist':
00509 playlist = self.tab.obj
00510 self.printout_item_list(playlist.getView())
00511 elif self.selection_type == 'downloads':
00512 self.printout_item_list(views.downloadingItems, views.pausedItems)
00513 elif self.selection_type == 'channel-folder':
00514 folder = self.tab.obj
00515 allItems = views.items.filterWithIndex(
00516 indexes.itemsByChannelFolder, folder)
00517 allItemsSorted = allItems.sort(folder.itemSort.sort)
00518 self.printout_item_list(allItemsSorted)
00519 allItemsSorted.unlink()
00520 else:
00521 raise ValueError("Unknown tab type")
00522
00523 @run_in_event_loop
00524 def do_downloads(self, line):
00525 """downloads -- Selects the downloads tab."""
00526 self.tab = FakeTab("statictab", "downloadtab")
00527 self.tab_changed()
00528
00529 def printout_item_list(self, *views):
00530 totalItems = 0
00531 for view in views:
00532 totalItems += len(view)
00533 if totalItems > 0:
00534 print "%-20s %-10s %s" % ("State", "Size", "Name")
00535 print "-" * 70
00536 for view in views:
00537 for item in view:
00538 state = item.get_state()
00539 if state == 'downloading':
00540 state += ' (%0.0f%%)' % item.download_progress()
00541 print "%-20s %-10s %s" % (state, item.get_size_for_display(),
00542 item.get_title())
00543 print
00544 else:
00545 print "No items"
00546
00547 def _get_item_view(self):
00548 if self.selection_type == 'feed':
00549 feed = self.tab.obj
00550 return feed.items
00551 elif self.selection_type == 'playlist':
00552 playlist = self.tab.obj
00553 return playlist.getView()
00554 elif self.selection_type == 'downloads':
00555 return views.downloadingItems
00556 elif self.selection_type == 'channel-folder':
00557 folder = self.tab.obj
00558 return views.items.filterWithIndex(indexes.itemsByChannelFolder,
00559 folder)
00560 else:
00561 raise ValueError("Unknown selection type")
00562
00563
00564 def _find_item(self, line):
00565 line = line.lower()
00566 for item in self._get_item_view():
00567 if item.get_title().lower() == line:
00568 return item
00569
00570 @run_in_event_loop
00571 def do_stop(self, line):
00572 """stop <name> -- Stops download by name."""
00573 if self.selection_type is None:
00574 print "Error: No feed/playlist selected"
00575 return
00576 item = self._find_item(line)
00577 if item is None:
00578 print "No item named %r" % line
00579 return
00580 if item.get_state() in ('downloading', 'paused'):
00581 item.expire()
00582 else:
00583 print '%s is not being downloaded' % item.get_title()
00584
00585 @run_in_event_loop
00586 def complete_stop(self, text, line, begidx, endidx):
00587 return self.handle_item_complete(text, self._get_item_view(),
00588 lambda i: i.get_state() in ('downloading', 'paused'))
00589
00590 @run_in_event_loop
00591 def do_download(self, line):
00592 """download <name> -- Downloads an item by name in the feed/playlist selected."""
00593 if self.selection_type is None:
00594 print "Error: No feed/playlist selected"
00595 return
00596 item = self._find_item(line)
00597 if item is None:
00598 print "No item named %r" % line
00599 return
00600 if item.get_state() == 'downloading':
00601 print '%s is currently being downloaded' % item.get_title()
00602 elif item.is_downloaded():
00603 print '%s is already downloaded' % item.get_title()
00604 else:
00605 item.download()
00606
00607 @run_in_event_loop
00608 def complete_download(self, text, line, begidx, endidx):
00609 return self.handle_item_complete(text, self._get_item_view(),
00610 lambda i: i.is_downloadable())
00611
00612 @run_in_event_loop
00613 def do_pause(self, line):
00614 """pause <name> -- Pauses a download by name."""
00615 if self.selection_type is None:
00616 print "Error: No feed/playlist selected"
00617 return
00618 item = self._find_item(line)
00619 if item is None:
00620 print "No item named %r" % line
00621 return
00622 if item.get_state() == 'downloading':
00623 item.pause()
00624 else:
00625 print '%s is not being downloaded' % item.get_title()
00626
00627 @run_in_event_loop
00628 def complete_pause(self, text, line, begidx, endidx):
00629 return self.handle_item_complete(text, self._get_item_view(),
00630 lambda i: i.get_state() == 'downloading')
00631
00632 @run_in_event_loop
00633 def do_resume(self, line):
00634 """resume <name> -- Resumes a download by name."""
00635 if self.selection_type is None:
00636 print "Error: No feed/playlist selected"
00637 return
00638 item = self._find_item(line)
00639 if item is None:
00640 print "No item named %r" % line
00641 return
00642 if item.get_state() == 'paused':
00643 item.resume()
00644 else:
00645 print '%s is not a paused download' % item.get_title()
00646
00647 @run_in_event_loop
00648 def complete_resume(self, text, line, begidx, endidx):
00649 return self.handle_item_complete(text, self._get_item_view(),
00650 lambda i: i.get_state() == 'paused')
00651
00652 @run_in_event_loop
00653 def do_rm(self, line):
00654 """rm <name> -- Removes an item by name in the feed/playlist selected."""
00655 if self.selection_type is None:
00656 print "Error: No feed/playlist selected"
00657 return
00658 item = self._find_item(line)
00659 if item is None:
00660 print "No item named %r" % line
00661 return
00662 if item.is_downloaded():
00663 item.expire()
00664 else:
00665 print '%s is not downloaded' % item.get_title()
00666
00667 @run_in_event_loop
00668 def complete_rm(self, text, line, begidx, endidx):
00669 return self.handle_item_complete(text, self._get_item_view(),
00670 lambda i: i.is_downloaded())
00671
00672 @run_in_event_loop
00673 def do_testdialog(self, line):
00674 """testdialog -- Tests the cli dialog system."""
00675 d = dialogs.ChoiceDialog("Hello", "I am a test dialog",
00676 dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL)
00677 def callback(dialog):
00678 print "TEST CHOICE: %s" % dialog.choice
00679 d.run(callback)
00680
00681 @run_in_event_loop
00682 def do_dumpdatabase(self, line):
00683 """dumpdatabase -- Dumps the database."""
00684 from miro import database
00685 print "Dumping database...."
00686 database.defaultDatabase.liveStorage.dumpDatabase(database.defaultDatabase)
00687 print "Done."
00688