From 944268c201ac8e8ca1986a81c471f34dcf78ede3 Mon Sep 17 00:00:00 2001 From: "R.D. Vaughan" Date: Fri, 8 Jul 2011 16:30:31 -0400 Subject: [PATCH] Add Miro 4.0.2 support to MiroBridge Change Log: 1) Added support for Miro 4.0.2 2) Silenced verbose output from ffmpeg when creating screen shots 3) Fixed oldrecording deletion abort when a source issues two videos that have identical publish dates and durations. Trapped error so no abort but also added every other unique DB field to deletion statment. 4) Minor error message corrections --- .../contrib/imports/mirobridge/mirobridge.py | 96 ++- .../mirobridge_interpreter_4_0_2.py | 772 ++++++++++++++++++ 2 files changed, 846 insertions(+), 22 deletions(-) create mode 100644 mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py diff --git a/mythtv/contrib/imports/mirobridge/mirobridge.py b/mythtv/contrib/imports/mirobridge/mirobridge.py index a48f5cba2a6..ba1133b09c8 100755 --- a/mythtv/contrib/imports/mirobridge/mirobridge.py +++ b/mythtv/contrib/imports/mirobridge/mirobridge.py @@ -190,6 +190,8 @@ # Add support for Miro version 3.5.x # Fixed screenshot code due to changes in ffmpeg. First # noticed in Ubuntu 10.10 (ffmepg v 0.6-4:0.6-2ubuntu6) +# Fixed aborts caused by bad metadata in Miro (videoFilename) +# Added support for Miro 4.0.2 examples_txt=u''' For examples, please see the Mirobridge's wiki page at http://www.mythtv.org/wiki/MiroBridge @@ -345,7 +347,10 @@ def __getattr__(self, attr): pass # Set up gettext before everything else + from miro import config # New for Miro4 (Changed import location) + from miro import eventloop # New for Miro4 from miro import gtcache + config.load() # New for Miro4 gtcache.init() # This fixes some/all problems with Python 2.6 support but should be @@ -359,33 +364,55 @@ def __getattr__(self, attr): from miro import prefs from miro import startup - from miro import config from miro import app from miro.frontends.cli.events import EventHandler + + # Required for Miro 4 as the configuration calls changed location and additional Miro 4 specific imports are required + try: + dummy = app.config.get(prefs.APP_VERSION) # A test to see if this is Miro v4 before the version can be read. If there is no exception this is Miro v4 + eventloop.setup_config_watcher() + from miro import signals + from miro import messages + from miro import eventloop + from miro import feed + from miro import workerprocess + from miro.frontends.cli.application import InfoUpdaterCallbackList + from miro.frontends.cli.application import InfoUpdater + from miro.plat.renderers.gstreamerrenderer import movie_data_program_info + miroConfiguration = app.config.get + from miro import controller + app.controller = controller.Controller() + except: + miroConfiguration = config.get + pass + except Exception, e: logger.critical(u"Importing Miro functions has an issue. Miro must be installed and functional, error(%s)", e) sys.exit(1) -logger.info(u"Miro Bridge version %s with Miro version %s" % (__version__, config.get(prefs.APP_VERSION))) -if config.get(prefs.APP_VERSION) < u"2.0.3": +logger.info(u"Miro Bridge version %s with Miro version %s" % (__version__, miroConfiguration(prefs.APP_VERSION))) +if miroConfiguration(prefs.APP_VERSION) < u"2.0.3": logger.critical(u"Your version of Miro (v%s) is not recent enough. Miro v2.0.3 is the minimum and it is preferred that you upgrade to Miro v2.5.2 or later.") sys.exit(1) try: - if config.get(prefs.APP_VERSION) < u"2.5.2": + if miroConfiguration(prefs.APP_VERSION) < u"2.5.2": logger.info("Using mirobridge_interpreter_2_0_3") from mirobridge.mirobridge_interpreter_2_0_3 import MiroInterpreter - elif config.get(prefs.APP_VERSION) < u"3.0": + elif miroConfiguration(prefs.APP_VERSION) < u"3.0": logger.info("Using mirobridge_interpreter_2_5_2") from mirobridge.mirobridge_interpreter_2_5_2 import MiroInterpreter - elif config.get(prefs.APP_VERSION) < u"3.5": + elif miroConfiguration(prefs.APP_VERSION) < u"3.5": logger.info("Using mirobridge_interpreter_3_0_0") from mirobridge.mirobridge_interpreter_3_0_0 import MiroInterpreter - else: + elif miroConfiguration(prefs.APP_VERSION) < u"4.0": logger.info("Using mirobridge_interpreter_3_5_0") from mirobridge.mirobridge_interpreter_3_5_0 import MiroInterpreter + else: + logger.info("Using mirobridge_interpreter_4_0_2") + from mirobridge.mirobridge_interpreter_4_0_2 import MiroInterpreter except Exception, e: - logger.critical(u"Importing mirobridge functions has failed. The following mirobridge files must be in the subdirectory 'mirobridge'.\n'mirobridge_interpreter_2_0_3.py' and 'mirobridge_interpreter_2_5_2.py', error(%s)" % e) + logger.critical(u"Importing mirobridge functions has failed. At least a 'mirobridge_interpreter' file that matches your Miro version must be in the subdirectory 'mirobridge'.\n'e.g. mirobridge_interpreter_2_0_3.py', 'mirobridge_interpreter_2_5_2.py' ... etc, error(%s)" % e) sys.exit(1) def _can_int(x): @@ -833,7 +860,7 @@ def setUseroptions(): channel_new_watch_copy[filter(is_not_punct_char, option.lower())] = cfg.get(section, option) for key in channel_new_watch_copy.keys(): if not channel_new_watch_copy[key].startswith(vid_graphics_dirs[u'mythvideo']): - logger.critical(u"All 'new->watch->copy' channel (%s) directory (%s) must be a subrirectory of the MythVideo base directory (%s)." % (key, channel_new_watch_copy[key], vid_graphics_dirs[u'mythvideo'])) + logger.critical(u"All 'new->watch->copy' channel (%s) directory (%s) must be a subdirectory of the MythVideo base directory (%s)." % (key, channel_new_watch_copy[key], vid_graphics_dirs[u'mythvideo'])) sys.exit(1) if channel_new_watch_copy[key][-1] != u'/': channel_new_watch_copy[key]+=u'/' @@ -979,7 +1006,7 @@ def takeScreenShot(videofile, screenshot_filename, size_limit=False, just_demens cmd2 = cmd % (videofile, delay, width, height, screenshot_filename) - return subprocess.call(u'%s > /dev/null' % cmd2, shell=True) + return subprocess.call(u'%s > /dev/null 2>/dev/null' % cmd2, shell=True) # end takeScreenShot() @@ -1136,7 +1163,10 @@ def getOldrecordedOrphans(): if simulation: logger.info(u"Simulation: Remove orphaned oldrecorded record (%s - %s)" % (data[u'title'], data[u'subtitle'])) else: - delOldRecorded((channel_id, data['starttime'])).delete() + try: # Sometimes a channel issues videos with identical publishing (starttime) dates. Try to using additiional details to identify the correct oldrecord. + delOldRecorded((channel_id, data['starttime'], data['endtime'], data['title'], data['subtitle'], data['description'])).delete() + except: + pass # Attempt a clean up for orphaned recorded video files and/or graphics (dirName, fileName) = os.path.split(u'%s%s - %s.%s' % (vid_graphics_dirs[u'default'], data[u'title'], data[u'subtitle'], u'png')) (fileBaseName, fileExtension)=os.path.splitext(fileName) @@ -2080,8 +2110,8 @@ def main(): # Validate settings ## Video base directory and current version and revision numbers - base_video_dir = config.get(prefs.MOVIES_DIRECTORY) - miro_version_rev = u"%s r%s" % (config.get(prefs.APP_VERSION), config.get(prefs.APP_REVISION_NUM)) + base_video_dir = miroConfiguration(prefs.MOVIES_DIRECTORY) + miro_version_rev = u"%s r%s" % (miroConfiguration(prefs.APP_VERSION), miroConfiguration(prefs.APP_REVISION_NUM)) displayMessage(u"Miro Version (%s)" % (miro_version_rev)) displayMessage(u"Base Miro Video Directory (%s)" % (base_video_dir,)) @@ -2095,8 +2125,16 @@ def main(): else: sys.exit(1) - if config.get(prefs.APP_VERSION) < u"2.0.3": - logger.critical(u"The installed version of Miro (%s) is too old. It must be at least v2.0.3 or higher." % config.get(prefs.APP_VERSION)) + if miroConfiguration(prefs.APP_VERSION) < u"2.0.3": + logger.critical(u"The installed version of Miro (%s) is too old. It must be at least v2.0.3 or higher." % miroConfiguration(prefs.APP_VERSION)) + if test_environment: + requirements_are_met = False + else: + sys.exit(1) + + # Miro 4.0.1 has a critical bug that effects MiroBridge. Miro must be upgraded. + if miroConfiguration(prefs.APP_VERSION) == u"4.0.1": + logger.critical(u"The installed version of Miro (%s) must be upgraded to Miro version 4.0.2 or higher." % miroConfiguration(prefs.APP_VERSION)) if test_environment: requirements_are_met = False else: @@ -2104,8 +2142,8 @@ def main(): # Verify that the import opml option can be used if opts.import_opml: - if config.get(prefs.APP_VERSION) < u"2.5.2": - logger.critical(u"The OPML import option requires Miro v2.5.2 or higher your Miro (%s) is too old." % config.get(prefs.APP_VERSION)) + if miroConfiguration(prefs.APP_VERSION) < u"2.5.2": + logger.critical(u"The OPML import option requires Miro v2.5.2 or higher your Miro (%s) is too old." % miroConfiguration(prefs.APP_VERSION)) if test_environment: requirements_are_met = False else: @@ -2164,7 +2202,7 @@ def main(): if opts.sleeptime: if not _can_int(opts.sleeptime): - logger.critical(u"Auto-dewnload sleep time (%s) must be numeric." % str(opts.sleeptime)) + logger.critical(u"Auto-download sleep time (%s) must be numeric." % str(opts.sleeptime)) if test_environment: requirements_are_met = False else: @@ -2240,9 +2278,15 @@ def main(): # Start the Miro Front and Backend - This allows mirobridge to execute actions on the Miro backend # displayMessage(u"Starting Miro Frontend and Backend") - startup.initialize(config.get(prefs.THEME_NAME)) + startup.initialize(miroConfiguration(prefs.THEME_NAME)) + if miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4 + app.info_updater = InfoUpdater() app.cli_events = EventHandler() app.cli_events.connect_to_signals() + + if miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4 + startup.install_first_time_handler(app.cli_events.handle_first_time) + startup.startup() app.cli_events.startup_event.wait() if app.cli_events.startup_failure: @@ -2250,6 +2294,11 @@ def main(): app.controller.shutdown() time.sleep(5) # Let the shutdown processing complete sys.exit(1) + + if miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4 + app.movie_data_program_info = movie_data_program_info + messages.FrontendStarted().send_to_backend() + app.cli_interpreter = MiroInterpreter() if opts.verbose: app.cli_interpreter.verbose = True @@ -2258,11 +2307,13 @@ def main(): app.cli_interpreter.simulation = opts.simulation app.cli_interpreter.videofiles = [] app.cli_interpreter.downloading = False - app.cli_interpreter.icon_cache_dir = config.get(prefs.ICON_CACHE_DIRECTORY) + app.cli_interpreter.icon_cache_dir = miroConfiguration(prefs.ICON_CACHE_DIRECTORY) app.cli_interpreter.imagemagick = imagemagick app.cli_interpreter.statistics = statistics - if config.get(prefs.APP_VERSION) < u"2.5.0": + if miroConfiguration(prefs.APP_VERSION) < u"2.5.0": app.renderer = app.cli_interpreter + elif miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4 + pass else: app.movie_data_program_info = app.cli_interpreter.movie_data_program_info @@ -2290,7 +2341,8 @@ def main(): if not opts.no_autodownload: if opts.verbose: app.cli_interpreter.verbose = False - app.cli_interpreter.do_mythtv_getunwatched(u'') + if miroConfiguration(prefs.APP_VERSION) < u"4.0": # Miro 4 automatically refreshes feeds and downloads + app.cli_interpreter.do_mythtv_update_autodownload(u'') before_download = len(app.cli_interpreter.videofiles) if opts.verbose: app.cli_interpreter.verbose = True diff --git a/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py b/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py new file mode 100644 index 00000000000..3ebe5d9de8c --- /dev/null +++ b/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py @@ -0,0 +1,772 @@ +# Miro - an RSS based video player application +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 +# Participatory Culture Foundation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. + +import cmd +import threading +import time +import Queue + +from miro import app +from miro import dialogs +from miro import eventloop +from miro import item +from miro import folder +from miro import tabs +from miro.frontends.cli import clidialog +from miro.plat import resources + +############################################################################################################ +# +# Start of mirobridge.py import additions - All to get feed updates, auto downloads and OPML import working +# +############################################################################################################ +import os, sys, subprocess, re, fnmatch, string +import logging +from miro import moviedata +from miro import commandline +from miro import autodler +from miro import downloader +from miro import iconcache +from miro.clock import clock +from miro import feed +from miro.commandline import parse_command_line_args +from miro import fileutil +from miro import autoupdate +from miro import startup +from miro import filetypes +from miro import messages +from miro import config +from miro import prefs +from miro import workerprocess +############################################################################################################ +# +# End of mirobridge.py import additions - All to get feed updates, auto downloads and OPML import working +# +############################################################################################################ + +def run_in_event_loop(func): + def decorated(*args, **kwargs): + return_hack = [] + event = threading.Event() + def runThenSet(): + try: + return_hack.append(func(*args, **kwargs)) + finally: + event.set() + eventloop.add_urgent_call(runThenSet, 'run in event loop') + event.wait() + if return_hack: + return return_hack[0] + decorated.__doc__ = func.__doc__ + return decorated + +class FakeTab: + def __init__(self, tab_type, tab_template_base): + self.type = tab_type + self.tab_template_base = tab_template_base + +class MiroInterpreter(cmd.Cmd): + def __init__(self): + cmd.Cmd.__init__(self) + self.quit_flag = False + self.tab = None + self.init_database_objects() + + def emptyline(self): + print "Type \"help\" for help." + + @run_in_event_loop + def init_database_objects(self): + self.feed_tabs = tabs.TabOrder.feed_order() + self.playlist_tabs = tabs.TabOrder.playlist_order() + self.tab_changed() + + def tab_changed(self): + """Calculate the current prompt. This method access database objects, + so it should only be called from the backend event loop + """ + if self.tab is None: + self.prompt = "> " + self.selection_type = None + + elif self.tab.type == 'feed': + if isinstance(self.tab, folder.ChannelFolder): + self.prompt = "channel folder: %s > " % self.tab.get_title() + self.selection_type = 'channel-folder' + else: + self.prompt = "channel: %s > " % self.tab.get_title() + self.selection_type = 'feed' + + elif self.tab.type == 'playlist': + self.prompt = "playlist: %s > " % self.tab.get_title() + self.selection_type = 'playlist' + + elif (self.tab.type == 'statictab' and + self.tab.tab_template_base == 'downloadtab'): + self.prompt = "downloads > " + self.selection_type = 'downloads' + else: + raise ValueError("Unknown tab type") + + def postcmd(self, stop, line): + # HACK + # If the last command results in a dialog, give it a little time to + # pop up + time.sleep(0.1) + while True: + try: + dialog = app.cli_events.dialog_queue.get_nowait() + except Queue.Empty: + break + clidialog.handle_dialog(dialog) + + return self.quit_flag + + def do_help(self, line): + """help -- Lists commands and help.""" + commands = [m for m in dir(self) if m.startswith("do_")] + for mem in commands: + docstring = getattr(self, mem).__doc__ + print " ", docstring.strip() + + def do_quit(self, line): + """quit -- Quits Miro cli.""" + self.quit_flag = True + + @run_in_event_loop + def do_feed(self, line): + """feed -- Selects a feed by name.""" + for tab in self.feed_tabs.get_all_tabs(): + if tab.get_title() == line: + self.tab = tab + self.tab.type = "feed" + self.tab_changed() + return + print "Error: %s not found." % line + + @run_in_event_loop + def do_rmfeed(self, line): + """rmfeed -- Deletes a feed.""" + for tab in self.feed_tabs.get_all_tabs(): + if tab.get_title() == line: + tab.remove() + return + print "Error: %s not found." % line + + @run_in_event_loop + def complete_feed(self, text, line, begidx, endidx): + return self.handle_tab_complete(text, + list(self.feed_tabs.get_all_tabs())) + + @run_in_event_loop + def complete_rmfeed(self, text, line, begidx, endidx): + return self.handle_tab_complete(text, + list(self.feed_tabs.get_all_tabs())) + + @run_in_event_loop + def complete_playlist(self, text, line, begidx, endidx): + return self.handle_tab_complete(text, + self.playlist_tabs.get_all_tabs()) + + def handle_tab_complete(self, text, view_items): + text = text.lower() + matches = [] + for tab in view_items: + if tab.get_title().lower().startswith(text): + matches.append(tab.get_title()) + return matches + + def handle_item_complete(self, text, view, filterFunc=lambda i: True): + text = text.lower() + matches = [] + for item_ in view: + if (item_.get_title().lower().startswith(text) and + filterFunc(item_)): + matches.append(item_.get_title()) + return matches + + ################################################################################### + # + # Start of mirobridge specific routines - Part 1 + # + ################################################################################### + @run_in_event_loop + def do_mythtv_check_downloading(self, line): + """Check if any items are being downloaded. Set True or False""" + self.downloading = False + downloadingItems = item.Item.only_downloading_view() + count = downloadingItems.count() + for it in downloadingItems: + logging.info(u"(%s - %s) video is downloading with (%0.0f%%) complete" % (it.get_channel_title(True).replace(u'/',u'-'), it.get_title().replace(u'/',u'-'), it.download_progress())) + if not count: + logging.info(u"No items downloading") + if count: + self.downloading = True + + @run_in_event_loop + def do_mythtv_import_opml(self, filename): + """Import an OPML file""" + try: + messages.ImportFeeds(filename).send_to_backend() + logging.info(u"Import of OPML file (%s) sent to Miro" % (filename)) + except Exception, e: + logging.info(u"Import of OPML file (%s) failed, error (%s)" % (filename, e)) + + @run_in_event_loop + def do_mythtv_updatewatched(self, line): + """Process MythTV update watched videos""" + items = item.Item.downloaded_view() + for video in self.videofiles: + for it in items: + if it.get_filename() == video: + break + else: + logging.info(u"Item for Miro video (%s) not found, skipping" % video) + continue + if self.simulation: + logging.info(u"Simulation: Item (%s - %s) marked as seen and watched" % (it.get_channel_title(True), it.get_title())) + else: + it.mark_item_seen(mark_other_items=True) + self.statistics[u'Miro_marked_watch_seen']+=1 + logging.info(u"Item (%s - %s) marked as seen and watched" % (it.get_channel_title(True), it.get_title())) + + @run_in_event_loop + def do_mythtv_getunwatched(self, line): + """Process MythTV get all un-watched video details""" + if self.verbose: + print + print u"Getting details on un-watched Miro videos" + self.videofiles = [] + if item.Item.unwatched_downloaded_items().count(): + if self.verbose: + print u"%-20s %-10s %s" % (u"State", u"Size", u"Name") + print u"-" * 70 + for it in item.Item.unwatched_downloaded_items(): + # Skip any audio file as MythTV Internal player may abort the MythTV Frontend on a MP3 + #if not it.isVideo: ##### This field has been removed from an item in Miro v2.5.2 + # continue + state = it.get_state() + if not state == u'newly-downloaded': + continue + # Skip any bittorrent video downloads for legal concerns + if hasattr(it.get_parent(), u'url'): + if filetypes.is_torrent_filename(it.get_parent().url): + continue + self.printItems(it) + self.videofiles.append(self._get_item_dict(it)) + if self.verbose: + print + if not len(self.videofiles): + logging.info(u"No un-watched Miro videos") + + @run_in_event_loop + def do_mythtv_getwatched(self, line): + """Process MythTV get all watched/saved video details""" + if self.verbose: + print + print u"Getting details on watched/saved Miro videos" + self.videofiles = [] + if item.Item.downloaded_view().count(): + if self.verbose: + print u"%-20s %-10s %s" % (u"State", u"Size", u"Name") + print u"-" * 70 + for it in item.Item.downloaded_view(): + state = it.get_state() + if state == u'newly-downloaded': + continue + # Skip any bittorrent video downloads for legal concerns + if hasattr(it.get_parent(), u'url'): + if filetypes.is_torrent_filename(it.get_parent().url): + continue + self.printItems(it) + self.videofiles.append(self._get_item_dict(it)) + if self.verbose: + print + if not len(self.videofiles): + logging.info(u"No watched/saved Miro videos") + + ########################################################### + # START - Function used for debugging - DO NOT UNCOMMENT + # + ########################################################### +# @run_in_event_loop +# def do_mythtv_test_views(self, line): +# """Process MythTV get all watched/saved video details""" +# print +# print u"Testing various views" +# print + +# print "Download view count (%s)" % item.Item.downloaded_view().count() +# print "Newly Download view count (%s)" % item.Item.newly_downloaded_view().count() +# print "Tab View Download view count (%s)" % item.Item.download_tab_view().count() +# print "unwatched Download view count (%s)" % item.Item.unwatched_downloaded_items().count() +# print "unique Download view count (%s)" % item.Item.unique_new_video_view().count() + +# print + +# if item.Item.downloaded_view().count(): +# print u"%-20s %-10s %s" % (u"State", u"Size", u"Name") +# print u"-" * 70 +# for it in item.Item.downloaded_view(): +# state = it.get_state() +# self.printItems(it) +# print + ########################################################### + # END - Function used for debugging - DO NOT UNCOMMENT + # + ########################################################### + + def printItems(self, it): + if not self.verbose: + return + state = it.get_state() + if state == u'downloading': + state += u' (%0.0f%%)' % it.download_progress() + print u"%-20s %-10s %s" % (state, it.get_size_for_display(), + it.get_title()) + # end printItems() + + @run_in_event_loop + def do_mythtv_item_remove(self, args): + """Removes an item from Miro by file name or Channel and title""" + for it in item.Item.downloaded_view(): + if isinstance(args, list): + if not args[0] or not args[1]: + continue + if filter(self.is_not_punct_char, it.get_channel_title(True).lower()) == filter(self.is_not_punct_char, args[0].lower()) and (filter(self.is_not_punct_char, it.get_title().lower())).startswith(filter(self.is_not_punct_char, args[1].lower())): + break + elif args: + if filter(self.is_not_punct_char, it.get_filename().lower()) == filter(self.is_not_punct_char, args.lower()): + break + else: + logging.info(u"No item named %s" % args) + return + if it.is_downloaded(): + if self.simulation: + logging.info(u"Simulation: Item (%s - %s) has been removed from Miro" % (it.get_channel_title(True), it.get_title())) + else: + it.expire() + self.statistics[u'Miro_videos_deleted']+=1 + logging.info(u'%s has been removed from Miro' % it.get_title()) + else: + logging.info(u'%s is not downloaded' % it.get_title()) + + def _get_item_dict(self, it): + """Take an item and convert all elements into a dictionary + return a dictionary of item elements + """ + def compatibleGraphics(filename): + if filename: + (dirName, fileName) = os.path.split(filename) + (fileBaseName, fileExtension)=os.path.splitext(fileName) + if not fileExtension[1:] in [u"png", u"jpg", u"bmp", u"gif"]: + return u'' + else: + return filename + else: + return u'' + + def useImageMagick(screenshot): + """ Using ImageMagick's utility 'identify'. Decide whether the screen shot is worth using. + >>> useImageMagick('identify screenshot.jpg') + >>> Example returned information "rose.jpg JPEG 640x480 DirectClass 87kb 0.050u 0:01" + >>> u'' if the screenshot quality is too low + >>> screenshot if the quality is good enough to use + """ + if not self.imagemagick: # If imagemagick is not installed do not bother checking + return u'' + + width_height = re.compile(u'''^(.+?)[ ]\[?([0-9]+)x([0-9]+)[^\\/]*$''', re.UNICODE) + p = subprocess.Popen(u'identify "%s"' % (screenshot), shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + + response = p.stdout.readline() + if response: + match = width_height.match(response) + if match: + dummy, width, height = match.groups() + width, height = int(width), int(height) + if width >= 320: + return screenshot + return u'' + else: + return u'' + return screenshot + # end useImageMagick() + + item_icon_filename = it.icon_cache.filename + channel_icon = it.get_feed().icon_cache.get_filename() + + # Conform to maximum length for MythTV database fields title and subtitle + maximum_length = 128 + channel_title = it.get_channel_title(True).replace(u'/',u'-') + if channel_title: + if len(channel_title) > maximum_length: + channel_title = channel_title[:maximum_length] + channel_title = channel_title.replace(u'"', u'') # These characters mess with filenames + title = it.get_title().replace(u'/',u'-') + if title: + if len(title) > maximum_length: + title = title[:maximum_length] + title = title.replace(u'"', u'') # These characters mess with filenames + title = title.replace(u"'", u'') # These characters mess with filenames + + item_dict = {u'feed_id': it.feed_id, u'parent_id': it.parent_id, u'isContainerItem': it.isContainerItem, u'seen': it.seen, u'autoDownloaded': it.autoDownloaded, u'pendingManualDL': it.pendingManualDL, u'downloadedTime': it.downloadedTime, u'watchedTime': it.watchedTime, u'pendingReason': it.pendingReason, u'title': title, u'expired': it.expired, u'keep': it.keep, u'videoFilename': it.get_filename(), u'eligibleForAutoDownload': it.eligibleForAutoDownload, u'duration': it.duration, u'screenshot': it.screenshot, u'resumeTime': it.resumeTime, u'channelTitle': channel_title, u'description': it.get_description(), u'size': it._get_size(), u'releasedate': it.get_release_date(), u'length': it.get_duration_value(), u'channel_icon': channel_icon, u'item_icon': item_icon_filename} + + if not item_dict[u'screenshot']: + if item_dict[u'item_icon']: + item_dict[u'screenshot'] = useImageMagick(item_dict[u'item_icon']) + + for key in [u'screenshot', u'channel_icon', u'item_icon']: + if item_dict[key]: + item_dict[key] = compatibleGraphics(item_dict[key]) + #if self.verbose: + # if item_dict[key]: + # print "item (%s - %s) %s (%s)" % (channel_title, title, key, item_dict[key]) + # else: + # print "item (%s - %s) does NOT have a %s" % (channel_title, title, key) + + #print item_dict + return item_dict + + # Two routines used for Channel title search and matching + def is_punct_char(self, char): + '''check if char is punctuation char + return True if char is punctuation + return False if char is not punctuation + ''' + return char in string.punctuation + + def is_not_punct_char(self, char): + '''check if char is not punctuation char + return True if char is not punctuation + return False if chaar is punctuation + ''' + return not self.is_punct_char(char) + + ################################################################################### + # + # End of mythbridge specific routines - Part 1 + # + ################################################################################### + + def _print_feeds(self, feeds): + current_folder = None + for tab in feeds: + if isinstance(tab, folder.ChannelFolder): + current_folder = tab + elif tab.get_folder() is not current_folder: + current_folder = None + if current_folder is None: + print " * " + tab.get_title() + elif current_folder is tab: + print " * [Folder] %s" % tab.get_title() + else: + print " * - %s" % tab.get_title() + + @run_in_event_loop + def do_feeds(self, line): + """feeds -- Lists all feeds.""" + print "FEEDS" + self._print_feeds(self.feed_tabs.get_all_tabs()) + + @run_in_event_loop + def do_update(self, line): + """update -- Updates all the feeds.""" + for mem in self.feed_tabs.get_all_tabs(): + print "telling %s to update" % mem.get_title() + mem.update() + + @run_in_event_loop + def do_play(self, line): + """play -- Plays an item by name in an external player.""" + if self.selection_type is None: + print "Error: No feed/playlist selected." + return + item_ = self._find_item(line) + if item_ is None: + print "No item named %r" % line + return + if item_.is_downloaded(): + resources.open_file(item_.get_filename()) + else: + print '%s is not downloaded' % item_.get_title() + + @run_in_event_loop + def do_playlists(self, line): + """playlists -- Lists all playlists.""" + for tab in self.playlistTabs.getView(): + print tab.obj.get_title() + + @run_in_event_loop + def do_playlist(self, line): + """playlist -- Selects a playlist.""" + for tab in self.playlistTabs.getView(): + if tab.obj.get_title() == line: + self.tab = tab + self.tab_changed() + return + print "Error: %s not found." % line + + @run_in_event_loop + def do_items(self, line): + """items -- Lists the items in the feed/playlist/tab selected.""" + if self.selection_type is None: + print "Error: No tab/feed/playlist selected." + return + elif self.selection_type == 'feed': + feed = self.tab + view = feed.items + self.printout_item_list(view) + elif self.selection_type == 'playlist': + playlist = self.tab.obj + self.printout_item_list(playlist.getView()) + elif self.selection_type == 'downloads': + self.printout_item_list(item.Item.downloading_view(), + item.Item.paused_view()) + elif self.selection_type == 'channel-folder': + folder = self.tab.obj + allItems = views.items.filterWithIndex( + indexes.itemsByChannelFolder, folder) + allItemsSorted = allItems.sort(folder.itemSort.sort) + self.printout_item_list(allItemsSorted) + allItemsSorted.unlink() + else: + raise ValueError("Unknown tab type") + + @run_in_event_loop + def do_downloads(self, line): + """downloads -- Selects the downloads tab.""" + self.tab = FakeTab("statictab", "downloadtab") + self.tab_changed() + + def printout_item_list(self, *views): + totalItems = 0 + for view in views: + totalItems += view.count() + if totalItems > 0: + print "%-20s %-10s %s" % ("State", "Size", "Name") + print "-" * 70 + for view in views: + for item in view: + state = item.get_state() + if state == 'downloading': + state += ' (%0.0f%%)' % item.download_progress() + print "%-20s %-10s %s" % (state, + item.get_size_for_display(), + item.get_title()) + print + else: + print "No items" + + def _get_item_view(self): + if self.selection_type == 'feed': + return item.Item.visible_feed_view(self.tab.id) + elif self.selection_type == 'playlist': + return item.Item.playlist_view(self.tab.id) + elif self.selection_type == 'downloads': + return item.Item.downloading_view() + elif self.selection_type == 'channel-folder': + folder = self.tab + return item.Item.visible_folder_view(folder.id) + else: + raise ValueError("Unknown selection type") + + def _find_item(self, line): + line = line.lower() + for item in self._get_item_view(): + if item.get_title().lower() == line: + return item + + @run_in_event_loop + def do_stop(self, line): + """stop -- Stops download by name.""" + if self.selection_type is None: + print "Error: No feed/playlist selected." + return + item = self._find_item(line) + if item is None: + print "No item named %r" % line + return + if item.get_state() in ('downloading', 'paused'): + item.expire() + else: + print '%s is not being downloaded' % item.get_title() + + @run_in_event_loop + def complete_stop(self, text, line, begidx, endidx): + return self.handle_item_complete(text, self._get_item_view(), + lambda i: i.get_state() in ('downloading', 'paused')) + + @run_in_event_loop + def do_download(self, line): + """download -- Downloads an item by name in the feed/playlist + selected. + """ + if self.selection_type is None: + print "Error: No feed/playlist selected." + return + item = self._find_item(line) + if item is None: + print "No item named %r" % line + return + if item.get_state() == 'downloading': + print '%s is currently being downloaded' % item.get_title() + elif item.is_downloaded(): + print '%s is already downloaded' % item.get_title() + else: + item.download() + + @run_in_event_loop + def complete_download(self, text, line, begidx, endidx): + return self.handle_item_complete(text, self._get_item_view(), + lambda i: i.is_downloadable()) + + @run_in_event_loop + def do_pause(self, line): + """pause -- Pauses a download by name.""" + if self.selection_type is None: + print "Error: No feed/playlist selected." + return + item = self._find_item(line) + if item is None: + print "No item named %r" % line + return + if item.get_state() == 'downloading': + item.pause() + else: + print '%s is not being downloaded' % item.get_title() + + @run_in_event_loop + def complete_pause(self, text, line, begidx, endidx): + return self.handle_item_complete(text, self._get_item_view(), + lambda i: i.get_state() == 'downloading') + + @run_in_event_loop + def do_resume(self, line): + """resume -- Resumes a download by name.""" + if self.selection_type is None: + print "Error: No feed/playlist selected." + return + item = self._find_item(line) + if item is None: + print "No item named %r" % line + return + if item.get_state() == 'paused': + item.resume() + else: + print '%s is not a paused download' % item.get_title() + + @run_in_event_loop + def complete_resume(self, text, line, begidx, endidx): + return self.handle_item_complete(text, self._get_item_view(), + lambda i: i.get_state() == 'paused') + + @run_in_event_loop + def do_rm(self, line): + """rm -- Removes an item by name in the feed/playlist selected. + """ + if self.selection_type is None: + print "Error: No feed/playlist selected." + return + item = self._find_item(line) + if item is None: + print "No item named %r" % line + return + if item.is_downloaded(): + item.expire() + else: + print '%s is not downloaded' % item.get_title() + + @run_in_event_loop + def complete_rm(self, text, line, begidx, endidx): + return self.handle_item_complete(text, self._get_item_view(), + lambda i: i.is_downloaded()) + + @run_in_event_loop + def do_testdialog(self, line): + """testdialog -- Tests the cli dialog system.""" + d = dialogs.ChoiceDialog("Hello", "I am a test dialog", + dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL) + def callback(dialog): + print "TEST CHOICE: %s" % dialog.choice + d.run(callback) + + ################################################################################### + # + # Start of mirobridge specific routines - Part 2 + # + ################################################################################### +@eventloop.idle_iterator +def clear_icon_cache_orphans(): + # delete icon_cache rows from the database with no associated + # item/feed/guide. + removed_objs = [] + for ic in iconcache.IconCache.orphaned_view(): + logging.warn("No object for IconCache: %s. Discarding", ic) + ic.remove() + removed_objs.append(str(ic.url)) + if removed_objs: + databaselog.info("Removed IconCache objects without an associated " + "db object: %s", ','.join(removed_objs)) + yield None + + # delete files in the icon cache directory that don't belong to IconCache + # objects. + + cachedir = fileutil.expand_filename(app.config.get( + prefs.ICON_CACHE_DIRECTORY)) + if not os.path.isdir(cachedir): + return + + existingFiles = [os.path.normcase(os.path.join(cachedir, f)) + for f in os.listdir(cachedir)] + yield None + + knownIcons = iconcache.IconCache.all_filenames() + yield None + + knownIcons = [ os.path.normcase(fileutil.expand_filename(path)) for path in + knownIcons] + yield None + + for filename in existingFiles: + if (os.path.exists(filename) + and os.path.basename(filename)[0] != '.' + and os.path.basename(filename) != 'extracted' + and not filename in knownIcons): + try: + os.remove(filename) + except OSError: + pass + yield None + ################################################################################### + # + # End of mirobridge specific routines - Part 2 + # + ###################################################################################