#!/usr/bin/env python # -*- coding: UTF-8 -*- # ---------------------- # Name: mirobridge.py Maintains MythTV database with Miro's downloaded # video files. # Python Script # Author: R.D. Vaughan # Purpose: This python script is intended to perform synchronise Miro's # video files with MythTV's "Watch Recordings" and MythVideo. # # The source of all video files is from those downloaded my Miro. # The source of all cover art and screen shoots are from those # downloaded and maintained by Miro. # Miro v2.03 or later must be already installed and configured # and already capable of downloading videos. # # Command line examples: # See help (-u and -h) options # # License:Creative Commons GNU GPL v2 # (http://creativecommons.org/licenses/GPL/2.0/) #------------------------------------- __title__ ="mirobridge - Maintains Miro's Video files with MythTV"; __author__="R.D.Vaughan" __purpose__=''' This python script is intended to synchronise Miro's video files with MythTV's "Watch Recordings" and MythVideo. The source of all video files are from those downloaded my Miro. The source of all meta data for the video files is from the Miro data base. The source of all cover art and screen shots are from those downloaded and maintained by Miro. Miro v2.0.3 or later must already be installed and configured and capable of downloading videos. ''' __version__=u"v0.7.1" # 0.1.0 Initial development # 0.2.0 Initial Alpha release for internal testing only # 0.2.1 Fixes from initial alpha test # Renamed imported micro python modules # 0.2.2 Fixed the duplicate video situation when Miro and a Channel feed has an issue which causes # the same video to be downloaded multiple time. This situation is now detected and protects # the duplicates from being added to either the MythTV or MythVideo. # Fixes a few problems with stripping HTML and converting HTML characters in Miro descriptions. # Changed the identification of HD and 720 videos to conform to MythTV standards # Removed the file "mirobridge_util.py" from the mirobridge distribution as the main Miro # distribution file "util.py" is used instead. # Changed ALL symlink Miro icons (coverart) to a copied Miro icon file, replace any coverart that # is currently a symlink to a copied Miro icon file and check that each MythVideo subdirectory # has an actual file that has the proper naming convention that supports storage groups. # Use using ImageMagick's utility 'mogrify' to convert Miro's screenshots to true png's (they are # really jpg files with a png extension) and reduce their size by 50% for the Watch Recordings # screen. Imagemagick is now a requirement to have installed. # Fixed a bug when either the Miro video's Channel title or title exceeded MythTV database 128 # characters limit for their equivalent title and subtitle fields. # When Miro has no screen shot for a Video substitute the item icon if it is high enough quality. # 0.2.3 All subdirectory coverfiles names are changed to "folder".(jpg/png) and gif files are converted. # Imagemagick is now mandatory as Miro has cover art which are gif types and must be converted # to either jpg or png so they will be recognised as folder coverart for Storage Groups. # Added if coverart from Miro is a gif type convert it to a jpg. # Removed resizing screenshots by 50% when adding a screen shot to the Watch Recordings screen. As # some graphics were already very small. # Fixed a bug were a Watch Recordings video was deleted by the user but the graphics files were not # also deleted. # Converted all mirobridge console messages to proper logger format (critical, error, warning, info) # Add changes as Anduin did to ttvdb.py to force 'utf8' output # Tested foreign language video's with their foreign language metadata - No problem # 0.2.4 Added a percentage downloaded message for each item that is downloading - updated every 30 secs. # Made Miro update and auto-download the default (no option -d). Added option (-n) to suppress # update and download processing. # If there is no screenshot then create one with ffmpeg. The size for Watch Recordings is 320 wide # and the mythtvideo screenshots are the same size as the video. From this point on this feature # will known as the "iamlindoro effect". # Fixed a bug where fold covers were being created even if a "folder.png" was already available. # As Anduin had done with ttvdb's support *.py files mirobridge's support *.py and example conf # files have been moved to a mirobridge subdirectory. # Added a check that makes sure that the Miro items are only video files. Audio files are skipped. # This is the final version that will support Miro v2.0.3 all higher versions will support # Miro v2.5.2 and higher. Except for small bugs this version will no longer be enhanced. # Added a small statistics report. # 0.2.5 Changes required for mirobridge to support Miro v2.5.2 have been made now mirobridge dynamically # supports both versions v2.0.3 and v2.5.2 # Fixed a statistics report bug for the copied to MythVideo totals and add new totals. # Fixed a bug where the Miro screen shot was not being resized for the Watch Recordings screen. # Fixed a bug when the Miro download time has not been set. In that case use the current date and # time. If this is not done the video cannot be deleted from Watch Recordings. # Fixed a bug where a video copied to MythVideo gets stranded in the Watch Recordings screen # even though the video file has been deleted in Miro. # Fixed a bug where a video that has been watched does not get removed from the Watch Recordings # screen. This stranded in the Watch Recordings screen even though the video file has been # deleted in Miro. This only happened when videos had been watched BUT there were no new videos # to add to the Watch Recordings screen. # Fixed a typo in the statistics report. # Fixed a Miro version check bug # Fixed a test environment option bug # Changed the "except" statement on imports to "except Exception:" as Anduin did to ttvdb.py # 0.3.0 Beta release # 0.3.1 Changed the check for Imagmagick convert utility and ffmpeg as the positive return value # is different on some distributions (0 or 1). The fail value is consistent (127). # Fixed the environment test option (-t) at times could give incorrect success message # 0.3.2 Fixed a bug when an empty item description would abort the script on an "extras" request # Fixed a bug when the Watched item processing failed message would abort the script # Fixed a bug when a user accidental deletes a video file symlink then the video symlink is NOT # recreated as it should have been. # Fixed a bug where double quotes in the title or subtitle caused issues with file names of graphics # Added to the statistics report the total number of Miro-MythVideo videos that will eventually be # expired and removed by Miro. # Added a info log message with the Miro Bridge version number, which may help in problem analysis. # Added trapping and diagnostic messages when the HTML tags could not be removed from a description. # Added a check that the installed "pyparsing" python library is at least v1.5.0 # Added detection of a Miro video deletion though the MythVideo UI. When this occurs # Miro is told to also deletes the video, graphics and meta data. # 0.3.3 Status change from Beta to production. No code changes # 0.3.4 Fixed when checking for orphaned videometadata records and there were no Miro videometadata # records. # Added additional detection and restrictions to the supported versions of Miro (minimum v2.0.3) # preferrably v2.5.2 or higher. # 0.3.5 Use the MythVideo.py binding rmMetadata() routine to delete old videometatdata records. # Added access checks for all directories that Miro Bridge needs to write # 0.3.6 Modifications as per requested changes. # 0.3.7 Fixed a bug with previous modifications that impacted Miro v2.0.3 only # 0.3.8 Fixed unicode errors with file names # Change to assist SG image hunting added the suffix "_coverart, _fanart, _banner, # _screenshot" respectively for any copied/created graphics. # 0.3.9 Fixed an issue when deleting a Miro video and the title/subtitle was not found due to special # characters. The search and matching is now more robust. # Fixed a bug where file name unicode errors caused an abort when creating screen shots # Removed from the mirobrodge.conf file the sections "[tv] and [movies]". This functionality # will be added to the Jamu v0.5.0 -MW option. # Added a check for locally available banners and fanart files when creating a MythVideo record. # This is added as Jamu v0.5.0 option -MW downloads graphics from TVDB and TMDB for Miro videos # when available. # Modified the check for mirobridge.conf to accomodate the needs of Mythbuntu. # Add mythcommflag seektable building for both recordings and mythvideo Miro videos. # 0.4.0 Fixed an abort where a variable had not been named properly due to a cut and paste error. # 0.4.1 Added a check that no other instance of Miro Bridge or Miro is already running. If there is # then post a critical error message and exit. # Do not add the Miro Bridge default banner when a Miro video has no subtitle as it overlaps # the title display on MythVideo information pop-ups. # Fixed a bug where a folder icon was being recreated even when it already existed. # 0.4.2 Added a missing import of "htmlentitydefs" which is rarely used in the description/plot XTML parsing # 0.4.3 Added support for audio type detection (2-channel, 6-channel, ...) that matches changes in ffmpeg. # ffmpeg is used in the detection of a video file's audio properties. # New Miro Videos added to the Default recordings directory have their names conform to MythTV standards # of "CHANID_ISODATETIME.ext". This resolves an obsure bug that caused orphaned screen shot graphics and # issues with Miro video deletions from the Watch Recordings screen if MiroBridge was run when the user # had MythTV started and in the Watch Recordings screen. # 0.4.4 Fixed a unicode issue with data read from a subprocess call. # Fixed an issue with the check for other instances of mirobridge.py running. # 0.4.5 Fixed a deletion issue when a Miro video subtitle contained more than 128 characters. # Disabled seek table creation as a number of the Miro video types (e.g. mov) do not work in MythTV with # seek tables. # 0.4.6 Changed "original air date" and "air date" to be Miro's item release date. This is more appropriate then # using download date as was done previously. Download date is still the fall back of there is no # release date. # 0.4.7 Changed all occurances of "strftime(u'" to "strftime('" as the unicode causes issues with python versions # less than 2.6 # 0.4.8 Some Miro "release" date values are not valid. Override with the current date. # 0.4.9 The ffmpeg SVN (e.g. SVN-r20151) is now outputting additional metadata, skip metadata that cannot be # processed. # 0.5.0 Correct the addition of adding hostnames to videometadata records and the use of relative paths when # there is no Videos Storage Group. # Added more informative error messages when trying to connect to the MythTV data base # 0.5.1 Fixed the config "all" options for command line -N and -M and config file sections [watch_then_copy] # and [mythvideo_only]. # Changed return codes from True to 0 and False to 1. # Added display of the directories that will be used by MiroBridge and whether they are storage groups. # Added file name sanitising logic plus a config file variable to add characters to be replaced by '_'. # The config file variable 'file name_char_filter' is required by users who save MiroBridge files on a # file system which only supports MS-Windows file naming conventions. # 0.5.2 Convert to new python bindings and replace all direct mysql data base calls. See ticket #7264 # Remove the requirement for the MySQLdb python library. # Remove all seek table processing as it is not used due to issues with some video file types. This code had been # previously disable but the code and related mythcommflag related code has been removed entirely. # Initialized new videometadata fields 'releasedate' and 'hash'. # 0.5.3 Fixed Exception messages # 0.5.4 Add the command line option (-i) to import an OPML file that was exported from Miro on a different PC. # This new option allows configuring Miro channels on a separate PC then importing those channels on # a Myth backend without a keyboard. No Miro GUI needs to run on the MythTV backend. # 0.5.5 Fixed bug #8051 - creating hash value for non-SG Miro video files had an incorrect variable and missing # library import. # 0.5.6 Fixed an abort and subsequent process hang when displaying a critical Miro start-up error # 0.5.7 The "DeletesFollowLinks" setting is incompatible with MiroBridge processings. A check # and termination of MiroBridge with an appropriate error message has been added. # Added better system error messages when an IOError exception occurs # 0.5.8 Add support for Miro version 3.0 # 0.5.9 Update for changes in Python Bindings # 0.6.0 Fixed a issue when a Miro video's metadata does not have a title. It was being re-added and the # database title fields in several records was being left empty. # 0.6.1 Modifications to support MythTV python bindings changes # 0.6.2 Trapped possible unicode errors which would hang the MiroBridge process # 0.6.3 Pull hostname from python bindings instead of socket libraries # 0.6.4 MythTV python bindings changes # 0.6.5 Added support for Miro v3.5.x # Small internal document changes # 0.6.6 Fixed screenshot code due to changes in ffmpeg. First # noticed in Ubuntu 10.10 (ffmepg v 0.6-4:0.6-2ubuntu6) # 0.6.7 Added support for Miro v4.0.2 or higher # Integrate with the new metadata functionality in recordings. Now users can specify graphics for # Miro Channels or set the Miro Channel name to match an entry in ttvdb.com and MythTV will # download the artwork automatically. # Automatically convert any copied Miro videos with an inetref of '99999999' which was only required # for the defunct Jamu script. The category is changed to 'Video Cast" and the inetref is removed # unless there is a matching Recording rule. # Silenced verbose output from ffmpeg when creating a screenshot # Fixed delOldRecording abort when a Channel sends two videos with identical published date and time. # All starttimes are now unique. # Fixed aborts caused by bad metadata in Miro (videoFilename) # Fixed a minor bug when a video is marked as watched within Miro but was not being removed from # "Watched Recordings" until the video expired # Removed creation of "folder.png" graphics when creating directories as that is no longer used # by MythVideo # Fixed the options "-h, --help" command line display # 0.6.8 Sometimes Miro metadata has no video filename. Skip these invalid videos. # 0.6.9 Adjust to datetime issues with MythTV v0.26's move to UTC datatimes in DB # 0.7.0 Fix bug introduced with v0.6.9, ticket reported as #11219 and #11220 # 0.7.1 Added support for Miro 6.x. Miro 5.x can never be supported due to # miro cli support being broken. examples_txt=u''' For examples, please see the Mirobridge's wiki page at http://www.mythtv.org/wiki/MiroBridge ''' # Common function imports import sys, os, re, locale, subprocess, locale, ConfigParser, codecs, shutil, struct import fnmatch, string, time, logging, traceback, platform, fnmatch, ConfigParser from datetime import timedelta from optparse import OptionParser from socket import gethostbyname import formatter import htmlentitydefs # Set command line options and arguments parser = OptionParser(usage=u"%prog usage: mirobridge -huevstdociVHSCWM [parameters]\n") parser.add_option( "-e", "--examples", action="store_true", default=False, dest="examples", help=u"Display examples for executing the mirobridge script") parser.add_option( "-v", "--version", action="store_true", default=False, dest="version", help=u"Display version and author information") parser.add_option( "-s", "--simulation", action="store_true", default=False, dest="simulation", help=u"Simulation (dry run), no files are copied, symlinks created or MythTV data "\ u"bases altered. If option (-n) is NOT specified Miro auto downloads WILL take "\ u"place. See option (-n) help for details.") parser.add_option( "-t", "--testenv", action="store_true", default=False, dest="testenv", help=u"Test that the local environment can run all mirobridge functionality") parser.add_option( "-n", "--no_autodownload", action="store_true", default=False, dest="no_autodownload", help=u"Do not perform Miro Channel updates, video expiry and auto-downloadings. "\ u"Default is to perform all perform all Channel maintenance features.") parser.add_option( "-o", "--nosubdirs", action="store_true", default=False, dest="nosubdirs", help=u"Organise MythVideo's Miro directory WITHOUT Miro channel subdirectories. "\ u"The default is to have Channel subdirectories.") parser.add_option( "-c", "--channel", metavar="CHANNEL_ID:CHANNEL_NUM", default="", dest="channel", help=u'Specifies the channel id that is used for Miros unplayed recordings. Enter '\ u'as "xxxx:yyy". Default is 9999:999. Be warned that once you change the '\ u'default channel_id "9999" you must always use this option!') parser.add_option( "-i", "--import_opml", metavar="OPMLFILEPATH", default="", dest="import_opml", help=u'Import Miro exported OPML file containing Channel configurations. File '\ u'name must be a fully qualified path. This option is exclusive to Miro '\ u'v2.5.x and higher.') parser.add_option( "-V", "--verbose", action="store_true", default=False, dest="verbose", help=u"Display verbose messages when processing") parser.add_option( "-H", "--hostname", metavar="HOSTNAME", default="", dest="hostname", help=u"MythTV Backend hostname mirobridge is to up date") parser.add_option( "-S", "--sleeptime", metavar="SLEEP_DELAY_SECONDS", default="", dest="sleeptime", help=u"The amount of seconds to wait for an auto download to start.\nThe default "\ u"is 60 seconds, but this may need to be adjusted for slower Internet connections.") parser.add_option( "-C", "--addchannel", metavar="ICONFILE_PATH", default="OFF", dest="addchannel", help=u'Add a Miro Channel record to MythTV. This gets rid of the "#9999 #9999" '\ u'on the Watch Recordings screen and replaces it with the usual\nthe channel '\ u'number and channel name.\nThe default if not overridden by the (-c) option '\ u'is channel number 999.\nIf a filename and path is supplied it will be set '\ u'as the channels icon. Make sure your override channel number is NOT one of '\ u'your current MythTV channel numbers.\nThis option is typically only used '\ u'once as there can only be one Miro channel record at a time.') parser.add_option( "-N", "--new_watch_copy", action="store_true", default=False, dest="new_watch_copy", help=u'For ALL Miro Channels: Use the "Watch Recording" screen to watch new Miro '\ u'downloads then once watched copy the videos, icons, screen shot and metadata '\ u'to MythVideo. Once coping is complete delete the video from Miro.\nThis option '\ u'overrides any "mirobridge.conf" settings.') parser.add_option( "-W", "--watch_only", action="store_true", default=False, dest="watch_only", help=u'For ALL Miro Channels: Only use "Watch Recording" never move any Miro videos '\ u'to MythVideo.\nThis option overrides any "mirobridge.conf" settings.') parser.add_option( "-M", "--mythvideo_only", action="store_true", default=False, dest="mythvideo_only", help=u'For ALL Miro Channel videos: Copy newly downloaded Miro videos to MythVideo '\ u'and removed from Miro. These Miro videos never appear in the MythTV "Watch '\ u'Recording" screen.\nThis option overrides any "mirobridge.conf" settings.') # Global variables opts, args = parser.parse_args() # Command line arguments and options local_only = True dir_dict = {u'posterdir': u"VideoArtworkDir", u'bannerdir': u'mythvideo.bannerDir', u'fanartdir': u'mythvideo.fanartDir', u'episodeimagedir': u'mythvideo.screenshotDir', u'mythvideo': u'VideoStartupDir'} vid_graphics_dirs = {u'default' : u'', u'mythvideo': u'', u'posterdir' : u'', u'bannerdir': u'', u'fanartdir': u'', u'episodeimagedir': u'',} key_trans = {u'filename' : u'mythvideo', u'coverfile': u'posterdir', u'banner' : u'bannerdir', u'fanart' : u'fanartdir', u'screenshot': u'episodeimagedir'} graphic_suffix = {u'default' : u'', u'mythvideo': u'', u'posterdir': u'_coverart', u'bannerdir': u'_banner', u'fanartdir': u'_fanart', u'episodeimagedir': u'_screenshot',} graphic_path_suffix = u"%s%s%s.%s" graphic_name_suffix = u"%s%s.%s" storagegroupnames = {u'Default' : u'default', u'Videos' : u'mythvideo', u'Coverart': u'posterdir', u'Banners' : u'bannerdir', u'Fanart' : u'fanartdir', u'Screenshots': u'episodeimagedir'} storagegroups={} # The gobal dictionary is only populated with the current hosts storage group entries image_extensions = [u"png", u"jpg", u"bmp"] simulation = False verbose = False ffmpeg = True channel_id = 9999 channel_num = 999 symlink_filename_format = u"%s - %s" flat = False # The MythVideo Miro directory structure flat or channel subdirectories download_sleeptime = float(60) # The default value for the time to wait for auto downloading to start channel_icon_override = [] channel_watch_only = [] channel_mythvideo_only = {} channel_new_watch_copy = {} tv_channels = {} movie_trailers = [] test_environment = False requirements_are_met = True # Used with the (-t) test environment option imagemagick = True mythcommflag_recordings = u'%s -c %%s -s "%%s" --rebuild' # or u'mythcommflag -f "%s" --rebuild' mythcommflag_videos = u'%s --rebuild --video "%%s"' filename_char_filter = u"/%\000" emptyTitle = u'_NO_TITLE_From_Miro' emptySubTitle = u'_NO_SUBTITLE_From_Miro' # Initalize Report Statistics: statistics = {u'WR_watched': 0, u'Miro_marked_watch_seen': 0, u'Miro_videos_deleted': 0, u'Miros_videos_downloaded': 0, u'Miros_MythVideos_video_removed': 0, u'Miros_MythVideos_added': 0, u'Miros_MythVideos_copied': 0, u'Total_unwatched': 0, u'Total_Miro_expiring': 0, u'Total_Miro_MythVideos': 0} class OutStreamEncoder(object): """Wraps a stream with an encoder """ def __init__(self, outstream, encoding=None): self.out = outstream if not encoding: self.encoding = sys.getfilesystemencoding() else: self.encoding = encoding def write(self, obj): """Wraps the output stream, encoding Unicode strings with the specified encoding""" if isinstance(obj, unicode): self.out.write(obj.encode(self.encoding)) else: self.out.write(obj) def __getattr__(self, attr): """Delegate everything but write to the stream""" return getattr(self.out, attr) # Sub class sys.stdout and sys.stderr as a utf8 stream. Deals with print and stdout unicode issues sys.stdout = OutStreamEncoder(sys.stdout, 'utf8') sys.stderr = OutStreamEncoder(sys.stderr, 'utf8') # Create logger logger = logging.getLogger(u"mirobridge") logger.setLevel(logging.DEBUG) # Create console handler and set level to debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # Create formatter formatter = logging.Formatter(u"%(asctime)s - %(name)s - %(levelname)s - %(message)s") # Add formatter to ch ch.setFormatter(formatter) # Add ch to logger logger.addHandler(ch) # The pyparsing python library must be installed version 1.5.0 or higher try: from pyparsing import * import pyparsing if pyparsing.__version__ < "1.5.0": logger.critical(u"The python library 'pyparsing' must be at version 1.5.0 or higher. "\ u"Your version is v%s" % pyparsing.__version__) sys.exit(1) except Exception, e: logger.critical(u"The python library 'pyparsing' must be installed and be version 1.5.0 or "\ u"higher, error(%s)" % e) sys.exit(1) logger.info(u"Using python library 'pyparsing' version %s" % pyparsing.__version__) # Find out if the MythTV python bindings can be accessed and instances can be created try: #If the MythTV python interface is found, we can insert data directly to MythDB or #get the directories to store poster, fanart, banner and episode graphics. from MythTV import OldRecorded, Recorded, RecordedProgram, Record, Channel, \ MythDB, Video, MythVideo, MythBE, MythError, MythLog from MythTV.database import DBDataWrite from MythTV.utility import datetime mythdb = None mythbeconn = None try: #Create an instance of each: MythDB, MythVideo mythdb = MythDB() except MythError, e: print u'\n! Warning - %s' % e.args[0] filename = os.path.expanduser("~")+'/.mythtv/config.xml' if not os.path.isfile(filename): logger.critical(u'A correctly configured (%s) file must exist\n' % filename) else: logger.critical(u'Check that (%s) is correctly configured\n' % filename) sys.exit(1) except Exception, e: logger.critical(u"Creating an instance caused an error for one of: MythDB or "\ u"MythVideo, error(%s)\n" % e) sys.exit(1) localhostname = mythdb.gethostname() try: mythbeconn = MythBE(backend=localhostname, db=mythdb) except MythError, e: logger.critical(u'MiroBridge must be run on a MythTV backend, error(%s)' % e.args[0]) sys.exit(1) except Exception, e: logger.critical(u"MythTV python bindings could not be imported, error(%s)" % e) sys.exit(1) # Find out if the Miro python bindings can be accessed and instances can be created try: # Initialize locale as required for Miro v3.x try: # Setup gconf_name early on so we can load config values from miro.plat import utils utils.initialize_locale() except: 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 # re-addressed when we have a system with Python 2.6 to test with. # (bug 11262) if sys.version_info[0] == 2 and sys.version_info[1] > 5: import miro.feedparser import miro.storedatabase sys.modules['feedparser'] = miro.feedparser sys.modules['storedatabase'] = miro.storedatabase from miro import prefs from miro import startup 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: version = app.config.get(prefs.APP_VERSION) # A test to see if this is Miro v4 or v6 before the version can be read. # If there is no exception this is Miro v4 or v6 eventloop.setup_config_watcher() from miro import signals from miro import messages from miro import eventloop from miro import feed from miro import workerprocess if version[0] == '4': from miro.frontends.cli.application import InfoUpdaterCallbackList from miro.frontends.cli.application import InfoUpdater from miro.plat.renderers.gstreamerrenderer import movie_data_program_info else: from miro import util app.startup_timer = util.DebuggingTimer() utils.register_exec_prefix() # 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 "\ u"and functional, error(%s)", e) sys.exit(1) 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 "\ u"the minimum and it is preferred that you upgrade to Miro v2.5.2 or "\ u"later.") % miroConfiguration(prefs.APP_VERSION)) sys.exit(1) try: 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 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 miroConfiguration(prefs.APP_VERSION) < u"3.5": logger.info("Using mirobridge_interpreter_3_0_0") from mirobridge.mirobridge_interpreter_3_0_0 import MiroInterpreter 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 elif miroConfiguration(prefs.APP_VERSION) < u"5.0": logger.info("Using mirobridge_interpreter_4_0_2") from mirobridge.mirobridge_interpreter_4_0_2 import MiroInterpreter elif miroConfiguration(prefs.APP_VERSION) < u"6.0": logger.critical(''' Miro version 5.x cannot be supported due to that version not supporting a CLI mode. Use versions 4.0.2+ or 6.0+ but not any v5.''') sys.exit(1) else: logger.info("Using mirobridge_interpreter_6_0_0") from mirobridge.mirobridge_interpreter_6_0_0 import MiroInterpreter from mirobridge.metadata import MetaData except Exception, e: logger.critical(u"Importing mirobridge functions has failed. At least a 'mirobridge_interpreter' "\ u"file that matches your Miro version must be in the subdirectory 'mirobridge'.\n'"\ u"e.g. mirobridge_interpreter_2_0_3.py', 'mirobridge_interpreter_2_5_2.py' ... etc, "\ u"error(%s)" % e) sys.exit(1) def _can_int(x): """Takes a string, checks if it is numeric. >>> _can_int("2") True >>> _can_int("A test") False """ if x == None: return False try: int(x) except ValueError: return False else: return True # end _can_int def displayMessage(message): """Displays messages through stdout. Usually used with option (-V) verbose mode. returns nothing """ global verbose if verbose: logger.info(message) # end _displayMessage def getExtention(filename): """Get the graphic file extension from a filename return the file extension from the filename """ (dirName, fileName) = os.path.split(filename) (fileBaseName, fileExtension)=os.path.splitext(fileName) return fileExtension[1:] # end getExtention def sanitiseFileName(name): '''Take a file name and change it so that invalid or problematic characters are substituted with a "_" return a sanitised valid file name ''' global filename_char_filter if name == None or name == u'': return u'_' for char in filename_char_filter: name = name.replace(char, u'_') if name[0] == u'.': name = u'_'+name[1:] return name # end sanitiseFileName() def useImageMagick(cmd): """ Process graphics files using ImageMagick's utility 'mogrify'. >>> useImageMagick('convert screenshot.jpg -resize 50% screenshot.png') >>> 0 >>> -1 """ return subprocess.call(u'%s > /dev/null' % cmd, shell=True) # end useImageMagick() class singleinstance(object): ''' singleinstance - based on Windows version by Dragan Jovelic this is a Linux version that accomplishes the same task: make sure that only a single instance of an application is running. ''' def __init__(self, pidPath): ''' pidPath - full path/filename where pid for running application is to be stored. Often this is ./var/<pgmname>.pid ''' from os import kill self.pidPath=pidPath # # See if pidFile exists # if os.path.exists(pidPath): # # Make sure it is not a "stale" pidFile # try: pid=int(open(pidPath, 'r').read().strip()) # # Check list of running pids, if not running it is stale so # overwrite # try: kill(pid, 0) pidRunning = 1 except OSError: pidRunning = 0 if pidRunning: self.lasterror=True else: self.lasterror=False except: self.lasterror=False else: self.lasterror=False if not self.lasterror: # # Write my pid into pidFile to keep multiple copies of program from # running. # fp=open(pidPath, 'w') fp.write(str(os.getpid())) fp.close() def alreadyrunning(self): return self.lasterror def __del__(self): if not self.lasterror: import os os.unlink(self.pidPath) # end singleinstance() def isMiroRunning(): '''Check if Miro is running. Only one can be running at the same time. return True if Miro us already running return False if Miro is NOT running ''' try: p = subprocess.Popen(u'ps aux | grep "miro.real"', shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) except: return False while True: data = p.stdout.readline() if not data: return False try: data = unicode(data, 'utf8') except (UnicodeEncodeError, TypeError): pass if data.find(u'grep') != -1: continue if data.find(u'miro.real') != -1: logger.critical(u"Miro is already running and therefore Miro Bridge should not be run:") logger.critical(u"(%s)" % data) break return True #end isMiroRunning() # Two routines used for movie title search and matching def is_punct_char(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(char): '''check if char is not punctuation char return True if char is not punctuation return False if char is punctuation ''' return not is_punct_char(char) def delOldRecorded(chanid, starttime, endtime, title, \ subtitle, description): ''' This routine is not supported in the native python bindings as MiroBridge uses the oldrecorded table outside of its original intent. return nothing ''' sql_cmd = u"""DELETE FROM `mythconverg`.`oldrecorded` WHERE `oldrecorded`.`chanid` = '%s' AND `oldrecorded`.`starttime` = '%s' AND `oldrecorded`.`endtime` = '%s';""" sql_del_a_record(sql_cmd % (chanid, set_del_datatime(starttime), set_del_datatime(endtime))) # end delOldRecorded() def delRecorded(chanid, starttime): '''Just delete a recorded record. Never abort as sometimes a record may not exist. return nothing ''' sql_cmd = u"""DELETE FROM `mythconverg`.`recorded` WHERE `recorded`.`chanid` = %s AND `recorded`.`starttime` = '%s';""" sql_del_a_record(sql_cmd % (chanid, set_del_datatime(starttime))) # end delRecorded() def set_del_datatime(rec_time): ''' Set the SQL datetime so that the delRecorded and delOldrecorded methods use UTC datetime values. return rec_time datetime string ''' # return rec_time.astimezone( rec_time.UTCTZ()).strftime("%Y-%m-%d %H:%M:%S") # end set_del_datetime() def sql_del_a_record(sql_cmd): ## Get a MythTV data base cursor cursor = mythdb.cursor() cursor.execute(sql_cmd) cursor.close() # return def hashFile(filename): '''Create metadata hash values for mythvideo files return a hash value return u'' if the was an error with the video file or the video file length was zero bytes ''' # Use the MythVideo hashing protocol when the video is in a storage groups if filename[0] != u'/': hash_value = mythbeconn.getHash(filename, u'Videos') if hash_value == u'NULL': return u'' else: return hash_value # Use a local hashing routine when video is not in a Videos storage group # For original code: http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#Python try: longlongformat = 'q' # long long bytesize = struct.calcsize(longlongformat) f = open(filename, "rb") filesize = os.path.getsize(filename) hash = filesize if filesize < 65536 * 2: # Video file is too small return u'' for x in range(65536/bytesize): buffer = f.read(bytesize) (l_value,)= struct.unpack(longlongformat, buffer) hash += l_value hash = hash & 0xFFFFFFFFFFFFFFFF #to remain as 64bit number f.seek(max(0,filesize-65536),0) for x in range(65536/bytesize): buffer = f.read(bytesize) (l_value,)= struct.unpack(longlongformat, buffer) hash += l_value hash = hash & 0xFFFFFFFFFFFFFFFF f.close() returnedhash = "%016x" % hash return returnedhash except(IOError): # Accessing to this video file caused and error return u'' # end hashFile() def rtnAbsolutePath(relpath, filetype=u'mythvideo'): '''Check if there is a Storage Group for the file type (mythvideo, coverfile, banner, fanart, screenshot) and form an appropriate absolute path and file name. return an absolute path and file name return the relpath sting if the file does not actually exist in the absolute path location ''' if relpath == None or relpath == u'': return relpath # There is a chance that this is already an absolute path if relpath[0] == u'/': return relpath if not storagegroups.has_key(filetype): return relpath # The Videos storage group path does not exist at all the metadata entry is useless for directory in storagegroups[filetype]: abpath = u"%s/%s" % (directory, relpath) if os.path.isfile(abpath): # The file must actually exist locally return abpath else: return relpath # The relative path does not exist at all the metadata entry is useless # end rtnAbsolutePath def getStorageGroups(): '''Populate the storage group dictionary with the host's storage groups. return False if there is an error ''' records = mythdb.getStorageGroup(hostname=localhostname) if records: for record in records: if record.groupname in storagegroupnames.keys(): try: dirname = unicode(record.dirname, 'utf8') except (UnicodeDecodeError): logger.error((u"The local Storage group (%s) directory contained\ncharacters "\ u"that caused a UnicodeDecodeError. This storage group has been "\ u"rejected.") % (record.groupname)) continue # Skip any line that has non-utf8 characters in it except (UnicodeEncodeError, TypeError): dirname = record.dirname # assume already unicode and pass unchanged # Add a slash if missing to any storage group dirname if dirname[-1:] == u'/': storagegroups[storagegroupnames[record.groupname]] = dirname else: storagegroups[storagegroupnames[record.groupname]] = dirname+u'/' continue if len(storagegroups): # Verify that each storage group is an existing local directory storagegroup_ok = True for key in storagegroups.keys(): if not os.path.isdir(storagegroups[key]): logger.critical(u"The Storage group (%s) directory (%s) does not exist" % \ (key, storagegroups[key])) storagegroup_ok = False if not storagegroup_ok: sys.exit(1) # end getStorageGroups def getMythtvDirectories(): """ Get all video and graphics directories found in the MythTV DB and add them to the dictionary. Ignore any MythTV Frontend setting when there is already a storage group configured. """ # Stop processing if this local host has any storage groups global localhostname, vid_graphics_dirs, dir_dict, storagegroups, local_only, verbose # When there is NO SG for Videos then ALL graphics paths MUST be local paths set in the FE and accessible # from the backend if storagegroups.has_key(u'mythvideo'): local_only = False # Pick up storage groups first for key in storagegroups.keys(): vid_graphics_dirs[key] = storagegroups[key] for key in dir_dict.keys(): if key == u'default' or key == u'mythvideo': continue if not storagegroups.has_key(key): # Set fall back graphics directory to Videos vid_graphics_dirs[key] = storagegroups[u'mythvideo'] # Set fall back SG graphics directory to Videos storagegroups[key] = storagegroups[u'mythvideo'] else: local_only = True if storagegroups.has_key(u'default'): vid_graphics_dirs[u'default'] = storagegroups[u'default'] if local_only: logger.warning(u'There is no "Videos" Storage Group set so ONLY MythTV Frontend local '\ u'paths for videos and graphics that are accessible from this MythTV '\ u'Backend can be used.') for key in dir_dict.keys(): if vid_graphics_dirs[key]: continue graphics_dir = mythdb.settings[localhostname][dir_dict[key]] # Only use path from MythTV if one was found if key == u'mythvideo': if graphics_dir: tmp_directories = graphics_dir.split(u':') if len(tmp_directories): for i in range(len(tmp_directories)): tmp_directories[i] = tmp_directories[i].strip() if tmp_directories[i] != u'': if os.path.exists(tmp_directories[i]): if tmp_directories[i][-1] != u'/': tmp_directories[i]+=u'/' vid_graphics_dirs[key] = tmp_directories[i] break else: logger.error(u"MythVideo video directory (%s) does not exist(%s)" % \ (key, tmp_directories[i])) else: logger.error(u"MythVideo video directory (%s) is not set" % (key, )) if key != u'mythvideo': if graphics_dir and os.path.exists(graphics_dir): if graphics_dir[-1] != u'/': graphics_dir+=u'/' vid_graphics_dirs[key] = graphics_dir else: # There is the chance that MythTv DB does not have a dir logger.error(u"(%s) directory is not set or does not exist(%s)" % (key, dir_dict[key])) # Make sure there is a directory set for Videos and other graphics directories on this host dir_for_all = True for key in vid_graphics_dirs.keys(): if not vid_graphics_dirs[key]: logger.critical(u"There must be a directory for Videos and each graphics type "\ u"the (%s) directory is missing." % (key)) dir_for_all = False if not dir_for_all: sys.exit(1) # Make sure that there is read/write access to all the directories Miro Bridge uses access_issue = False for key in vid_graphics_dirs.keys(): if not os.access(vid_graphics_dirs[key], os.F_OK | os.R_OK | os.W_OK): logger.critical(u"\nEvery Video and graphics directory must be read/writable for "\ u"Miro Bridge to function. There is a permissions issue with (%s)." % \ (vid_graphics_dirs[key], )) access_issue = True if access_issue: sys.exit(1) # Print out the video and image directories that will be used for processing if verbose: dir_types={'posterdir' : "Cover art ", 'bannerdir': 'Banners ', 'fanartdir' : 'Fan art ', 'episodeimagedir': 'Screenshots', 'mythvideo': 'Video ', 'default': 'Default '} sys.stdout.write(u""" ========================================================================================== Listed below are the types and base directories that will be use for processing. The list reflects your current configuration for the '%s' back end and whether a directory is a 'SG' (storage group) or not. Note: All directories are from settings in the MythDB specific to hostname (%s). ------------------------------------------------------------------------------------------ """ % (localhostname, localhostname)) for key in vid_graphics_dirs.keys(): sg_flag = 'NO ' if storagegroups.has_key(key): if vid_graphics_dirs[key] == storagegroups[key]: sg_flag = 'YES' sys.stdout.write(u"Type: %s - SG-%s - Directory: (%s)\n" % \ (dir_types[key], sg_flag, vid_graphics_dirs[key])) sys.stdout.write(\ u"""------------------------------------------------------------------------------------------ If a directory you set from a separate Front end is not displayed it means that the directory is not accessible from this backend OR you must add the missing directories using the Front end on this Back end. Front end settings are host machine specific. ========================================================================================== """) # end getMythtvDirectories() def setUseroptions(): """ Change variables through a user supplied configuration file abort the script if there are issues with the configuration file values """ global simulation, verbose, channel_icon_override, channel_watch_only, channel_mythvideo_only global vid_graphics_dirs, tv_channels, movie_trailers, filename_char_filter useroptions=u"~/.mythtv/mirobridge.conf" if useroptions[0]==u'~': useroptions=os.path.expanduser(u"~")+useroptions[1:] if os.path.isfile(useroptions) == False: logger.info( u"There was no mirobridge configuration file found (%s)" % useroptions) return cfg = ConfigParser.SafeConfigParser() cfg.read(useroptions) for section in cfg.sections(): if section[:5] == u'File ': continue if section == u'variables': for option in cfg.options(section): if option == u'filename_char_filter': for char in cfg.get(section, option): filename_char_filter+=char continue if section == u'icon_override': # Add the Channel names to the array of Channels that are to use their item's icon for option in cfg.options(section): channel_icon_override.append(option) continue if section == u'watch_only': # Add the Channel names to the array of Channels that will only not be moved to MythVideo for option in cfg.options(section): if option == u'all miro channels': channel_watch_only = [u'all'] break else: channel_watch_only.append(filter(is_not_punct_char, option.lower())) continue if section == u'mythvideo_only': # Add the Channel names to the array of Channels that will be moved to MythVideo only for option in cfg.options(section): if option == u'all miro channels': channel_mythvideo_only[u'all'] = cfg.get(section, option) break else: channel_mythvideo_only[filter(is_not_punct_char, option.lower())] = \ cfg.get(section, option) for key in channel_mythvideo_only.keys(): if not channel_mythvideo_only[key].startswith(vid_graphics_dirs[u'mythvideo']): logger.critical((u"All Mythvideo only configuration (%s) directories (%s) must "\ u"be a subdirectory of the MythVideo base directory (%s).") % \ (key, channel_mythvideo_only[key], vid_graphics_dirs[u'mythvideo'])) sys.exit(1) if channel_mythvideo_only[key][-1] != u'/': channel_mythvideo_only[key]+=u'/' continue if section == u'watch_then_copy': # Add the Channel names to the array of Channels once watched will be copied to MythVideo for option in cfg.options(section): if option == u'all miro channels': channel_new_watch_copy[u'all'] = cfg.get(section, option) break else: 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 "\ u"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'/' continue # end setUserconfig def massageDescription(description, extras=False): '''Massage the Miro description removing all HTML. return the massaged description ''' def unescape(text): """Removes HTML or XML character references and entities from a text string. @param text The HTML (or XML) source text. @return The plain text, as a Unicode string, if necessary. from Fredrik Lundh 2008-01-03: input only unicode characters string. http://effbot.org/zone/re-sub.htm#unescape-html """ def fixup(m): text = m.group(0) if text[:2] == u"&#": # character reference try: if text[:3] == u"&#x": return unichr(int(text[3:-1], 16)) else: return unichr(int(text[2:-1])) except ValueError: logger.warn(u"Remove HTML or XML character references: Value Error") pass else: # named entity # reescape the reserved characters. try: if text[1:-1] == u"amp": text = u"&amp;" elif text[1:-1] == u"gt": text = u"&gt;" elif text[1:-1] == u"lt": text = u"&lt;" else: logger.info(u"%s" % text[1:-1]) text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) except KeyError: logger.warn(u"Remove HTML or XML character references: keyerror") pass return text # leave as is return re.sub(u"&#?\w+;", fixup, text) details = {} if not description: # Is there anything to massage if extras: details[u'plot'] = description return details else: return description director_text = u'Director: ' director_re = re.compile(director_text, re.UNICODE) ratings_text = u'Rating: ' ratings_re = re.compile(ratings_text, re.UNICODE) removeText = replaceWith("") scriptOpen,scriptClose = makeHTMLTags(u"script") scriptBody = scriptOpen + SkipTo(scriptClose) + scriptClose scriptBody.setParseAction(removeText) anyTag,anyClose = makeHTMLTags(Word(alphas,alphanums+u":_")) anyTag.setParseAction(removeText) anyClose.setParseAction(removeText) htmlComment.setParseAction(removeText) commonHTMLEntity.setParseAction(replaceHTMLEntity) # first pass, strip out tags and translate entities firstPass = (htmlComment | scriptBody | commonHTMLEntity | anyTag | anyClose ).transformString(description) # first pass leaves many blank lines, collapse these down repeatedNewlines = LineEnd() + OneOrMore(LineEnd()) repeatedNewlines.setParseAction(replaceWith(u"\n\n")) secondPass = repeatedNewlines.transformString(firstPass) text = secondPass.replace(u"Link to Catalog\n ",u'') text = unescape(text) if extras: text_lines = text.split(u'\n') for index in range(len(text_lines)): text_lines[index] = text_lines[index].rstrip() index+=1 else: text_lines = [text.replace(u'\n', u' ')] if extras: description = u'' for text in text_lines: if len(director_re.findall(text)): details[u'director'] = text.replace(director_text, u'') continue # probe the nature [...]Rating: 3.0/5 (1 vote cast) if len(ratings_re.findall(text)): data = text[text.index(ratings_text):].replace(ratings_text, u'') try: number = data[:data.index(u'/')] # HD trailers ratings are our of 5 not 10 like MythTV so must be multiplied by two try: details[u'userrating'] = float(number) * 2 except ValueError: details[u'userrating'] = 0.0 except: details[u'userrating'] = 0.0 text = text[:text.index(ratings_text)] if text.rstrip(): description+=text+u' ' else: description = text_lines[0].replace(u"[...]Rating:", u"[...] Rating:") if extras: details[u'plot'] = description.rstrip() return details else: return description # end massageDescription() def getOldrecordedOrphans(): """Retrieves the Miro oldrecorded records for localhostname. Match them against the Miro recorded records and identify any orphaned oldrecorded records. Those records mean a MythTV user deleted the Miro video from the Watched Recordings screen or from MythVideo. Delete the orphaned records from MythTV plus any left over graphic files. return array of oldrecorded dictionary records which are orphaned return None if there are no orphaned oldrecorded records """ global simulation, verbose, channel_id, localhostname, vid_graphics_dirs, statistics global channel_icon_override global graphic_suffix, graphic_path_suffix, graphic_name_suffix # Convert any old videmetadata copied videos changing their inetref and category values. # Prevents accidental deletions. metadata.convertOldMiroVideos() recorded_array = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname)) oldrecorded_array = list(mythdb.searchOldRecorded(chanid=channel_id, )) videometadata = list(mythdb.searchVideos(category=u'Miro')) orphans = [] for record in oldrecorded_array: for recorded in recorded_array: # First check if the recording was marked for deletion # by the user. The BE actually deletes the recorded record. if recorded['autoexpire'] == 9999: continue # if recorded[u'starttime'] == record[u'starttime'] and \ recorded[u'endtime'] == record[u'endtime']: break else: for video in videometadata: if video[u'title'] == record[u'title'] and \ video[u'subtitle'] == record[u'subtitle']: break else: orphans.append(record) for data in orphans: if simulation: logger.info( u"Simulation: Remove orphaned oldrecorded record (%s - %s)" % \ (data[u'title'], data[u'subtitle'])) else: 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']) except: pass # Attempt a clean up for orphaned recorded video files and/or graphics metadata.cleanupVideoAndGraphics(u'%s%s_%s.%s' % \ (vid_graphics_dirs[u'default'], channel_id, data[u'starttime'].strftime('%Y%m%d%H%M%S'), u'png')) # Attempt a clean up for orphaned MythVideo files and/or # graphics from the Default directory metadata.cleanupVideoAndGraphics(u'%s%s - %s.%s' % \ (vid_graphics_dirs[u'default'], data[u'title'], data[u'subtitle'], u'png')) # Attempt a clean up for orphaned MythVideo screenshot metadata.cleanupVideoAndGraphics(u'%s%s - %s%s.%s' % \ (vid_graphics_dirs[u'episodeimagedir'], data[u'title'], data[u'subtitle'], graphic_suffix[u'episodeimagedir'], u'png')) # Remove any unique cover art graphic files if data[u'title'].lower() in channel_icon_override: metadata.cleanupVideoAndGraphics(u'%s%s - %s%s.%s' % \ (vid_graphics_dirs[u'posterdir'], data[u'title'], data[u'subtitle'], graphic_suffix[u'posterdir'], u'png')) displayMessage( u"Removed orphaned Miro video and graphics files (%s - %s)" % \ (data[u'title'], data[u'subtitle'])) return orphans # end getOldrecordedOrphans() def getStartEndTimes(duration, downloadedTime): '''Calculate a videos start and end times and isotime for the recorded file name. return an array of initialised values if either duration or downloadedTime is invalid return an array of the video's start, end times and isotime ''' starttime = datetime.now() end = starttime start_end = [starttime.strftime('%Y-%m-%d %H:%M:%S'), starttime.strftime('%Y-%m-%d %H:%M:%S'), starttime.strftime('%Y%m%d%H%M%S')] if downloadedTime != None: try: dummy = downloadedTime.strftime('%Y-%m-%d') except ValueError: downloadedTime = datetime.now() end = downloadedTime+timedelta(seconds=duration) start_end[0] = downloadedTime.strftime('%Y-%m-%d %H:%M:%S') start_end[1] = end.strftime('%Y-%m-%d %H:%M:%S') start_end[2] = downloadedTime.strftime('%Y%m%d%H%M%S') # Check if there is a Miro video with an identical start time. # If there is incremement that start and end times by one until unique. while True: if not len(list(mythdb.searchOldRecorded(chanid=channel_id, starttime=start_end[0]))): break starttime = starttime + timedelta(0,1) end = end + timedelta(0,1) start_end[0] = starttime.strftime('%Y-%m-%d %H:%M:%S') start_end[1] = end.strftime('%Y-%m-%d %H:%M:%S') start_end[2] = starttime.strftime('%Y%m%d%H%M%S') continue return start_end # end getStartEndTimes() def setSymbolic(filename, storagegroupkey, symbolic_name, allow_symlink=False): '''Convert the file into a symbolic name according to it's storage group (there may be no storage group for the key). Check if a symbolic link exists and replace the link with a copy of the file. except for video files. Abort if the file does not exist. return the symbolic link to the file ''' global simulation, verbose, storagegroups, vid_graphics_dirs global graphic_suffix, graphic_path_suffix, graphic_name_suffix global local_only if not os.path.isfile(filename): logger.error(u"The file (%s) must exist to create a symbolic link" % filename) return None ext = getExtention(filename) if ext.lower() == u'm4v': ext = u'mpg' convert = False if ext.lower() in [u'gif', u'jpeg', u'JPG', ]: # convert graphics to standard jpg and png types ext = u'jpg' convert = True if storagegroupkey in storagegroups.keys() and storagegroupkey == u'default': sym_filepath = graphic_path_suffix % (storagegroups[storagegroupkey], symbolic_name, graphic_suffix[storagegroupkey], ext) sym_filename = graphic_name_suffix % (symbolic_name, graphic_suffix[storagegroupkey], ext) elif storagegroupkey in storagegroups.keys() and not local_only: sym_filepath = graphic_path_suffix % (storagegroups[storagegroupkey], symbolic_name, graphic_suffix[storagegroupkey], ext) sym_filename = graphic_name_suffix % (symbolic_name, graphic_suffix[storagegroupkey], ext) else: sym_filepath = graphic_path_suffix % (vid_graphics_dirs[storagegroupkey], symbolic_name, graphic_suffix[storagegroupkey], ext) sym_filename = sym_filepath if allow_symlink: if os.path.isfile(os.path.realpath(sym_filepath)): return sym_filename else: if os.path.isfile(os.path.realpath(sym_filepath)) and not os.path.islink(sym_filepath): return sym_filename # file already exists try: # Try to remove a broken symbolic link os.remove(sym_filepath) except OSError: pass if simulation: if allow_symlink: logger.info(u"Simulation: Used file (%s) to create symlink as (%s)" % \ (filename, sym_filepath)) else: logger.info(u"Simulation: Used file (%s) copy as (%s)" % (filename, sym_filepath)) return sym_filename try: if allow_symlink: os.symlink(filename, sym_filepath) displayMessage(u"Used file (%s) to created symlink (%s)" % (filename, sym_filepath)) else: try: # Every file but video files copied to support Storage groups if convert: useImageMagick(u'convert "%s" "%s"' % (filename, sym_filepath)) displayMessage(u"Convert and copy Miro file (%s) to file (%s)" % \ (filename, sym_filepath)) else: shutil.copy2(filename, sym_filepath) displayMessage(u"Copied Miro file (%s) to file (%s)" % (filename, sym_filepath)) except OSError, e: logger.critical((u"Trying to copy the Miro file (%s) to the file (%s).\nError(%s)"\ u"\nThis maybe a permissions error (mirobridge.py does not have "\ u"permission to write to the directory).") % \ (filename ,sym_filepath, e)) sys.exit(1) except OSError, e: logger.critical((u"Trying to create the Miro file (%s) symlink (%s).\nError(%s)\nThis "\ u"maybe a permissions error (mirobridge.py does not have permission to "\ u"write to the directory).") % (sym_filepath, e)) sys.exit(1) return sym_filename # end setSymbolic() def createOldRecordedRecord(item, start, end, inetref): '''Using the details from a Miro item record create a MythTV oldrecorded record return an array of MythTV of a full initialised oldrecorded record dictionaries ''' global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id tmp_oldrecorded={} # Create the oldrecorded dictionary tmp_oldrecorded[u'chanid'] = channel_id tmp_oldrecorded[u'starttime'] = start tmp_oldrecorded[u'endtime'] = end tmp_oldrecorded[u'title'] = item[u'channelTitle'] tmp_oldrecorded[u'subtitle'] = item[u'title'] tmp_oldrecorded[u'category'] = u'Miro' tmp_oldrecorded[u'station'] = u'MIRO' tmp_oldrecorded[u'inetref'] = inetref tmp_oldrecorded[u'season'] = item[u'season'] tmp_oldrecorded[u'episode'] = item[u'episode'] try: tmp_oldrecorded[u'description'] = massageDescription(item[u'description']) except TypeError: tmp_oldrecorded[u'description'] = item[u'description'] return tmp_oldrecorded # end createOldRecordedRecord() def createRecordedRecords(item): '''Using the details from a Miro item record create a MythTV recorded record return an array of MythTV full initialised recorded and recordedprogram record dictionaries ''' global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id tmp_recorded={} tmp_recordedprogram={} # Get any graphics that already exist graphics = metadata.getMetadata(sanitiseFileName(item[u'channelTitle'])) ffmpeg_details = metadata.getVideoDetails(item[u'videoFilename']) start_end = getStartEndTimes(ffmpeg_details[u'duration'], item[u'downloadedTime']) if item[u'releasedate'] == None: item[u'releasedate'] = item[u'downloadedTime'] try: dummy = item[u'releasedate'].strftime('%Y-%m-%d') except ValueError: item[u'releasedate'] = item[u'downloadedTime'] # Create the recorded dictionary tmp_recorded[u'chanid'] = channel_id tmp_recorded[u'starttime'] = start_end[0] tmp_recorded[u'endtime'] = start_end[1] tmp_recorded[u'title'] = item[u'channelTitle'] tmp_recorded[u'subtitle'] = item[u'title'] tmp_recorded[u'season'] = item[u'season'] tmp_recorded[u'episode'] = item[u'episode'] tmp_recorded[u'inetref'] = graphics[u'inetref'] try: tmp_recorded[u'description'] = massageDescription(item[u'description']) except TypeError: print print u"Channel title(%s) subtitle(%s)" % (item[u'channelTitle'], item[u'title']) print u"The 'massageDescription()' function could not remove HTML and XML tags from:" print u"Description (%s)\n\n" % item[u'description'] tmp_recorded[u'description'] = item[u'description'] tmp_recorded[u'category'] = u'Miro' tmp_recorded[u'hostname'] = localhostname tmp_recorded[u'lastmodified'] = tmp_recorded[u'endtime'] tmp_recorded[u'filesize'] = item[u'size'] if item[u'releasedate'] != None: tmp_recorded[u'originalairdate'] = item[u'releasedate'].strftime('%Y-%m-%d') basename = setSymbolic(item[u'videoFilename'], u'default', u"%s_%s" % \ (channel_id, start_end[2]), allow_symlink=True) if basename != None: tmp_recorded[u'basename'] = basename else: logger.critical(u"The file (%s) must exist to create a recorded record" % \ item[u'videoFilename']) sys.exit(1) tmp_recorded[u'progstart'] = start_end[0] tmp_recorded[u'progend'] = start_end[1] # Create the recordedpropgram dictionary tmp_recordedprogram[u'chanid'] = channel_id tmp_recordedprogram[u'starttime'] = start_end[0] tmp_recordedprogram[u'endtime'] = start_end[1] tmp_recordedprogram[u'title'] = item[u'channelTitle'] tmp_recordedprogram[u'subtitle'] = item[u'title'] try: tmp_recordedprogram[u'description'] = massageDescription(item[u'description']) except TypeError: tmp_recordedprogram[u'description'] = item[u'description'] tmp_recordedprogram[u'category'] = u"Miro" tmp_recordedprogram[u'category_type'] = u"series" if item[u'releasedate'] != None: tmp_recordedprogram[u'airdate'] = item[u'releasedate'].strftime('%Y') tmp_recordedprogram[u'originalairdate'] = item[u'releasedate'].strftime('%Y-%m-%d') tmp_recordedprogram[u'stereo'] = ffmpeg_details[u'stereo'] tmp_recordedprogram[u'hdtv'] = ffmpeg_details[u'hdtv'] tmp_recordedprogram[u'audioprop'] = ffmpeg_details[u'audio'] tmp_recordedprogram[u'videoprop'] = ffmpeg_details[u'video'] return [tmp_recorded, tmp_recordedprogram, createOldRecordedRecord(item, start_end[0], start_end[1], graphics[u'inetref'])] # end createRecordedRecord() def createVideometadataRecord(item): '''Using the details from a Miro item create a MythTV videometadata record return an dictionary of MythTV an initialised videometadata record ''' global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id global vid_graphics_dirs, channel_icon_override, flat, image_extensions global local_only ffmpeg_details = metadata.getVideoDetails(item[u'videoFilename']) start_end = getStartEndTimes(ffmpeg_details[u'duration'], item[u'downloadedTime']) sympath = u'Miro' if not flat: sympath+=u"/%s" % item[u'channelTitle'] # Get any graphics that already exist graphics = metadata.getMetadata(sanitiseFileName(item[u'channelTitle'])) videometadata = {} videometadata[u'title'] = item[u'channelTitle'] videometadata[u'subtitle'] = item[u'title'] videometadata[u'inetref'] = graphics[u'inetref'] videometadata[u'season'] = item[u'season'] videometadata[u'episode'] = item[u'episode'] try: details = massageDescription(item[u'description'], extras=True) except TypeError: print print u"MythVideo-Channel title(%s) subtitle(%s)" % \ (item[u'channelTitle'], item[u'title']) print u"The 'massageDescription()' function could not remove HTML and XML tags from:" print u"Description (%s)\n\n" % item[u'description'] details = {u'plot': item[u'description']} for key in details.keys(): videometadata[key] = details[key] if item[u'releasedate'] == None: item[u'releasedate'] = item[u'downloadedTime'] try: dummy = item[u'releasedate'].strftime('%Y-%m-%d') except ValueError: item[u'releasedate'] = item[u'downloadedTime'] if item[u'releasedate'] != None: videometadata[u'year'] = item[u'releasedate'].strftime('%Y') videometadata[u'releasedate'] = item[u'releasedate'].strftime('%Y-%m-%d') videometadata[u'length'] = ffmpeg_details[u'duration']/60 if item.has_key(u'copied'): videometadata[u'category'] = u'Video Cast' else: videometadata[u'category'] = u'Miro' if not item.has_key(u'copied'): videofile = setSymbolic(item[u'videoFilename'], u'mythvideo', "%s/%s - %s" % \ (sympath, sanitiseFileName(item[u'channelTitle']), sanitiseFileName(item[u'title'])), allow_symlink=True) if videofile != None: videometadata[u'filename'] = videofile if not local_only and videometadata[u'filename'][0] != u'/': videometadata[u'host'] = localhostname.lower() else: logger.critical(u"The file (%s) must exist to create a videometadata record" % \ item[u'videoFilename']) sys.exit(1) else: videometadata[u'filename'] = item[u'videoFilename'] if not local_only and videometadata[u'filename'][0] != u'/': videometadata[u'host'] = localhostname.lower() videometadata[u'hash'] = hashFile(videometadata[u'filename']) if not item.has_key(u'copied'): if graphics['coverart']: videometadata[u'coverfile'] = graphics['coverart'] elif item[u'channel_icon'] and not item[u'channelTitle'].lower() in channel_icon_override: filename = setSymbolic(item[u'channel_icon'], u'posterdir', u"%s" % \ (sanitiseFileName(item[u'channelTitle']))) if filename != None: videometadata[u'coverfile'] = filename else: if item[u'item_icon']: filename = setSymbolic(item[u'item_icon'], u'posterdir', u"%s - %s" % \ (sanitiseFileName(item[u'channelTitle']), sanitiseFileName(item[u'title']))) if filename != None: videometadata[u'coverfile'] = filename else: videometadata[u'coverfile'] = item[u'channel_icon'] if not item.has_key(u'copied'): if item[u'screenshot']: filename = setSymbolic(item[u'screenshot'], u'episodeimagedir', u"%s - %s" % \ (sanitiseFileName(item[u'channelTitle']), sanitiseFileName(item[u'title']))) if filename != None: videometadata[u'screenshot'] = filename else: if item[u'screenshot']: videometadata[u'screenshot'] = item[u'screenshot'] if graphics['banner']: videometadata[u'banner'] = graphics['banner'] if graphics['fanart']: videometadata[u'fanart'] = graphics['fanart'] return [videometadata, createOldRecordedRecord(item, start_end[0], start_end[1], graphics[u'inetref'])] # end createVideometadataRecord() def createChannelRecord(icon, channel_id, channel_num): ''' Create the optional Miro "channel" record as it makes the Watch Recordings screen look better. return True if the record was created and written successfully abort if the processing failed ''' global localhostname, simulation, verbose, vid_graphics_dirs if icon != u"": if not os.path.isfile(icon): logger.critical((u'The Miro channel icon file (%s) does not exist.\nThe variable '\ u'needs to be a fully qualified file name and path.\ne.g. '\ u'./mirobridge.py -C "/path to the channel icon/miro_channel_icon.'\ u'jpg"') % (icon)) sys.exit(1) data={} data['chanid'] = channel_id data['channum'] = str(channel_num) data['freqid'] = str(channel_num) data['atsc_major_chan'] = int(channel_num) data['icon'] = u'' if icon != u'': data['icon'] = icon data['callsign'] = u'Miro' data['name'] = u'Miro' data['last_record'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if simulation: logger.info(u"Simulation: Create Miro channel record channel_id(%d) and channel_num(%d)" % \ (channel_id, channel_num)) logger.info(u"Simulation: Channel icon file(%s)" % (icon)) else: try: Channel().create(data) except MythError, e: logger.critical((u"Failed writing the Miro channel record. Most likely the Channel "\ u"Id and number already exists.\nUse MythTV set up program "\ u"(mythtv-setup) to alter or remove the offending channel.\nYou "\ u"specified Channel ID (%d) and Channel Number (%d), error(%s)") % \ (channel_id, channel_num, e.args[0])) sys.exit(1) return True # end createChannelRecord(icon) def checkVideometadataFails(record, flat): ''' Verify that the real path exists for both video and graphics for this MythVideo Miro record. return False if there were no failures return True if a failure was found ''' global localhostname, verbose, vid_graphics_dirs, key_trans for field in key_trans.keys(): if not record[field]: continue if record[field] == u'No Cover': continue if not record[u'host'] or record[field][0] == u'/': if not os.path.isfile(record[field]): return True else: if not os.path.isfile(vid_graphics_dirs[key_trans[field]]+record[field]): return True return False # end checkVideometadataFails() def createMiroMythVideoDirectory(): '''If the "Miro" directory does not exist in MythVideo then create it. abort if there is an issue creating the directory ''' global localhostname, vid_graphics_dirs, storagegroups, channel_id, flat, simulation, verbose # Check that a MIRO video directory exists # if not then create the dir and add symbolic link to cover/file or icon miro = u'Miro' miro_path = vid_graphics_dirs[u'mythvideo']+miro if not os.path.isdir(miro_path): try: if simulation: logger.info(u"Simulation: Create Miro Mythvideo directory (%s)" % (miro_path,)) else: try: os.mkdir(miro_path) except OSError, e: logger.critical((u"Create Miro Mythvideo directory (%s).\nError(%s)\n" \ u"This may be due to a permissions error.") % \ (miro_path, e)) sys.exit(1) except OSError, e: logger.critical((u"Creation of MythVideo 'Miro' directory (%s) failed.\nError(%s)"\ u"\nThis may be due to a permissions error.") % (miro_path, e)) sys.exit(1) # end createMiroMythVideoDirectory() def createMiroChannelSubdirectory(item): '''Create the Miro Channel subdirectory in MythVideo abort if the subdirectory cannot be made ''' global localhostname, vid_graphics_dirs, storagegroups, channel_id, flat, simulation, verbose miro = u'Miro' path = u"%s%s/%s" % (vid_graphics_dirs[u'mythvideo'], miro, sanitiseFileName(item[u'channelTitle'])) if simulation: logger.info(u"Simulation: Make subdirectory(%s)" % (path)) else: if not os.path.isdir(path): try: os.mkdir(path) except OSError, e: logger.critical((u"Creation of MythVideo 'Miro' subdirectory path (%s) failed."\ u"\nError(%s)\nThis may be due to a permissions error.") % \ (path, e)) sys.exit(1) # end createMiroChannelSubdirectory() def getPlayedMiroVideos(): '''From the MythTV database recorded records identify all "played" Miro Video files. return None if there were either no Miro recorded records or none that were in "watched" status return an array of subtitles of those Miro video files that were "watched" ''' global localhostname, vid_graphics_dirs, storagegroups, verbose, channel_id, statistics filenames=[] recorded = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname)) for record in recorded: # Skip if the video has NOT been watched or has been marked for # deletion if record[u'watched'] == 0 or record['autoexpire'] == 9999: continue try: filenames.append(os.path.realpath(storagegroups[u'default']+record[u'basename'])) statistics[u'WR_watched']+=1 except OSError, e: logger.info(u"Miro video file has been removed (%s) outside of mirobridge\nError(%s)" % \ (storagegroups[u'default']+record[u'basename'], e)) continue displayMessage(u"Miro video (%s) (%s) has been marked as watched in MythTV." % \ (record[u'title'], record[u'subtitle'])) if len(filenames): return filenames else: return None # end getPlayedMiroVideos() def updateMythRecorded(items): ''' Add and delete MythTV (Watch Recordings) Miro recorded records. Add and delete symbolic links to coverart/Miro icons. Abort if processing failed return True if processing was successful ''' global localhostname, vid_graphics_dirs, storagegroups, channel_id, simulation, imagemagick global graphic_suffix, graphic_path_suffix, graphic_name_suffix if not items: # There may not be any new items but a clean up of existing recorded may be required items = [] items_copy = list(items) # Deal with existing Miro videos already in the MythTV data base recorded = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname)) for record in recorded: if storagegroups.has_key(u'default'): sym_filepath = u"%s%s" % (storagegroups[u'default'], record[u'basename']) else: sym_filepath = u"%s%s" % (vid_graphics_dirs[u'default'], record[u'basename']) # Remove any videos that were marked as viewed within Miro but NOT MythTV remove = False for item in items: if item[u'channelTitle'] == record[u'title'] and item[u'title'] == record[u'subtitle']: break else: remove = True # Remove any Miro related "watched" recordings (symlink, recorded records) # Or remove any Miro related with broken symbolic video links if record[u'watched'] == 1 or not os.path.isfile(os.path.realpath(sym_filepath)): remove = True if remove: displayMessage(u"Removing watched Miro recording (%s) (%s)" % \ (record[u'title'], record[u'subtitle'])) # Remove the database recorded and oldrecorded records if simulation: logger.info(u"Simulation: Remove recorded/recordedprogram/oldrecorded records and "\ u"associated Miro Video file for chanid(%s), starttime(%s)" % \ (record['chanid'], record['starttime'])) else: try: # Attempting to clean up an recorded record and # its associated video file (miro symlink) rtn = delRecorded(record['chanid'], record['starttime']) except MythError, e: pass # Clean up for recorded video files and/or graphics metadata.cleanupVideoAndGraphics(u'%s%s_%s.%s' % \ (vid_graphics_dirs[u'default'], record['chanid'], record[u'starttime'].strftime('%Y%m%d%H%M%S'), u'png')) try: # Attempting to clean up an orphaned oldrecorded record which may or may not exist rtn = delOldRecorded(record['chanid'], record['starttime'], record['endtime'], record['title'], record['subtitle'], record['description']) except Exception, e: pass recorded = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname)) for record in recorded: # Skip any item already in MythTV data base for item in items: if item[u'channelTitle'] == record[u'title'] and item[u'title'] == record[u'subtitle']: items_copy.remove(item) break # Add new Miro unwatched videos to MythTV'd data base for item in items_copy: # Do not create records for Miro video files when Miro has a corrupt or missing file name if item[u'videoFilename'] == None: continue # Do not create records for Miro video files that do not exist if not os.path.isfile(os.path.realpath(item[u'videoFilename'])): continue if not os.path.isfile(os.path.realpath(item[u'videoFilename'])): continue # Do not create records for Miro video files that do not exist records = createRecordedRecords(item) if records: if simulation: logger.info(u"Simulation: Added recorded and recordedprogram records for "\ u"(%s - %s)" % (item[u'channelTitle'], item[u'title'],)) else: try: Recorded().create(records[0]) RecordedProgram().create(records[1]) OldRecorded().create(records[2]) except MythError, e: logger.warning(u"Inserting recorded/recordedprogram/oldrecorded records "\ u"non-critical error (%s) for (%s - %s)" % \ (e.args[0], item[u'channelTitle'], item[u'title'])) else: logger.critical(u"Creation of recorded/recordedprogram/oldrecorded record data for "\ u"(%s - %s)" % (item[u'channelTitle'], item[u'title'],)) sys.exit(1) if item[u'channel_icon']: # Add Cover art link to channel icon ext = getExtention(item[u'channel_icon']) coverart_filename = u"%s%s%s.%s" % (vid_graphics_dirs[u'posterdir'], sanitiseFileName(item[u'channelTitle']), graphic_suffix[u'posterdir'], ext) if not os.path.isfile(os.path.realpath(coverart_filename)): # Make sure it does not exist if simulation: logger.info(u"Simulation: Remove symbolic link(%s)" % (coverart_filename,)) else: try: # Clean up broken symbolic link os.remove(coverart_filename) except OSError: pass if simulation: logger.info(u"Simulation: Create icon file(%s) cover art file(%s)" % \ (item[u'channel_icon'], coverart_filename)) else: try: shutil.copy2(item[u'channel_icon'], coverart_filename) displayMessage((u"Copied a Miro Channel Icon file (%s) to MythTV as "\ u"file (%s).") % (item[u'channel_icon'], coverart_filename)) except OSError, e: logger.critical((u"Copying an icon file(%s) to coverart file(%s) failed."\ u"\nError(%s)\nThis may be due to a permissions error.") % \ (item[u'channel_icon'], coverart_filename, e)) sys.exit(1) if item[u'screenshot'] and imagemagick: # Add Miro screen shot to 'Default' recordings directory screenshot_recorded = u"%s%s.png" % (vid_graphics_dirs[u'default'], records[0][u'basename']) if not os.path.isfile(screenshot_recorded): # Make sure it does not exist if simulation: logger.info(u"Simulation: Create screenshot file(%s) as(%s)" % \ (item[u'screenshot'], screenshot_recorded)) else: try: demensions = u'' try: demensions = takeScreenShot(item[u'videoFilename'], screenshot_recorded, size_limit=True, just_demensions=False) except: pass if demensions: demensions = u"-size %s" % demensions useImageMagick(u'convert "%s" %s "%s"' % \ (item[u'screenshot'], demensions, screenshot_recorded)) displayMessage((u"Used a Miro Channel screenshot file (%s) to\ncreate "\ u"using ImageMagick the MythTV Watch Recordings screen "\ u"shot file\n(%s).") % \ (item[u'screenshot'], screenshot_recorded)) except OSError, e: logger.critical((u"Creating screenshot file(%s) as(%s) failed.\nError"\ u"(%s)\nThis may be due to a permissions error.") % \ (item[u'screenshot'], screenshot_recorded, e)) sys.exit(1) else: screenshot_recorded = u"%s%s.png" % \ (vid_graphics_dirs[u'default'], records[0][u'basename']) try: takeScreenShot(item[u'videoFilename'], screenshot_recorded, size_limit=True) except: pass return True # updateMythRecorded() def updateMythVideo(items): '''Add and delete MythVideo records for played Miro Videos. Add and delete symbolic links to Miro Videos, to coverart/Miro icons, banners and Miro screenshots and fanart. NOTE: banner and fanart graphics were provided with the script and are used only if present. Abort if processing failed return True if processing was successful ''' global localhostname, vid_graphics_dirs, storagegroups, channel_id, flat, simulation, verbose global channel_watch_only, statistics global graphic_suffix, graphic_path_suffix, graphic_name_suffix global local_only if not items: # There may not be any new items but a clean up of existing records may be required items = [] # Check that a MIRO video directory exists # if not then create the dir and add symbolic link to cover/file or icon createMiroMythVideoDirectory() # Remove any Miro Mythvideo records which the video or graphics paths are broken records = list(mythdb.searchVideos(category=u'Miro')) statistics[u'Total_Miro_MythVideos'] = len(records) for record in records: # Count the Miro-MythVideos that Miro is expiring or has saved if record[u'filename'][0] == u'/': if os.path.islink(record[u'filename']) and os.path.isfile(record[u'filename']): statistics[u'Total_Miro_expiring']+=1 elif record[u'host'] and storagegroups.has_key(u'mythvideo'): if os.path.islink(storagegroups[u'mythvideo']+record[u'filename']) and \ os.path.isfile(storagegroups[u'mythvideo']+record[u'filename']): statistics[u'Total_Miro_expiring']+=1 for record in records: if checkVideometadataFails(record, flat): delete = False if os.path.islink(record[u'filename']): # Only delete video files if they are symlinks if not record[u'host'] or record[u'filename'][0] == '/': if not os.path.isfile(record[u'filename']): delete = True else: if not os.path.isfile(vid_graphics_dirs[key_trans[field]]+record[u'filename']): delete = True else: if not os.path.isfile(record[u'filename']): delete = True if delete: # Only delete video files if they are symlinks if simulation: logger.info(u"Simulation: DELETE videometadata for intid = %s" % \ (record[u'intid'],)) logger.info(u"Simulation: DELETE oldrecorded for title(%s), subtitle(%s)" % \ (record[u'title'], record[u'subtitle'])) else: rtn = Video(record[u'intid'], db=mythdb).delete() try: # An orphaned oldrecorded record may not exist for oldrecorded in mythdb.searchOldRecorded(title=record[u'title'], subtitle=record[u'subtitle'] ): rtn = delOldRecorded(channel_id, oldrecorded['starttime'], oldrecorded['endtime'], oldrecorded['title'], oldrecorded['subtitle'], oldrecorded['description']) except Exception, e: pass statistics[u'Total_Miro_MythVideos']-=1 # Remove video file metadata.deleteFile(record[u'filename'], record[u'host'], u'mythvideo') if record[u'screenshot']: # Remove any associated Screenshot metadata.deleteFile(record[u'screenshot'], record[u'host'], u'episodeimagedir') # Remove any unique cover art graphic files if record[u'title'].lower() in channel_icon_override: metadata.deleteFile(record[u'coverfile'], record[u'host'], u'posterdir') if not items: # There may not be any new items to add to MythVideo return True # Reread Miro Mythvideo videometadata records # Remove the matching videometadata record from array of items items_copy = list(items) records = list(mythdb.searchVideos(category=u'Miro')) for record in records: for item in items: if item[u'channelTitle'] == record[u'title'] and item[u'title'] == record[u'subtitle']: try: items_copy.remove(item) except ValueError: logger.info((u"Video (%s - %s) was found multiple times in list of (watched "\ u"and/or saved) items from Miro - skipping") % \ (item[u'channelTitle'], item[u'title'])) pass break for item in items: # Remove any items that are for a Channel that does not get MythVideo records if filter(is_not_punct_char, item[u'channelTitle'].lower()) in channel_watch_only: try: # Some items may have already been removed, let those passed items_copy.remove(item) except ValueError: pass # Add Miro videos that remain in the item list # If not a flat directory check if title directory exists and add icon symbolic link as coverfile for item in items_copy: if not flat and not item.has_key(u'copied'): createMiroChannelSubdirectory(item) if not item[u'screenshot']: # If there is no screen shot then create one screenshot_mythvideo = u"%s%s - %s%s.jpg" % \ (vid_graphics_dirs[u'episodeimagedir'], sanitiseFileName(item[u'channelTitle']), sanitiseFileName(item[u'title']), graphic_suffix[u'episodeimagedir']) try: result = takeScreenShot(item[u'videoFilename'], screenshot_mythvideo, size_limit=False) except: result = None if result != None: item[u'screenshot'] = screenshot_mythvideo tmp_array = createVideometadataRecord(item) videometadata = tmp_array[0] oldrecorded = tmp_array[1] if simulation: logger.info(u"Simulation: Create videometadata record for (%s - %s)" % \ (item[u'channelTitle'], item[u'title'])) else: # Check for duplicates if not local_only and videometadata[u'filename'][0] != u'/': intid = list(mythdb.searchVideos(exactfile=videometadata[u'filename'], host=localhostname.lower())) else: intid = list(mythdb.searchVideos(exactfile=videometadata[u'filename'])) if intid == []: # Check for an empty array try: intid = Video(db=mythdb).create(videometadata).intid except MythError, e: logger.critical(u"Adding Miro video to MythVideo (%s - %s) failed for (%s)." % \ (item[u'channelTitle'], item[u'title'], e.args[0])) sys.exit(1) if not item.has_key(u'copied'): try: OldRecorded().create(oldrecorded) except MythError, e: logger.critical(u"Failed writing the oldrecorded record for(%s)" % (e.args[0])) sys.exit(1) if videometadata[u'filename'][0] == u'/': cmd = mythcommflag_videos % videometadata[u'filename'] elif videometadata[u'host'] and storagegroups[u'mythvideo']: cmd = mythcommflag_videos % \ ((storagegroups[u'mythvideo']+videometadata[u'filename'])) statistics[u'Miros_MythVideos_added']+=1 statistics[u'Total_Miro_expiring']+=1 statistics[u'Total_Miro_MythVideos']+=1 displayMessage(u"Added Miro video to MythVideo (%s - %s)" % \ (videometadata[u'title'], videometadata[u'subtitle'])) else: sys.stdout.write(u'') displayMessage(\ u"""Skipped adding a duplicate Miro video to MythVideo: (%s - %s) Sometimes a Miro channel has the same video downloaded multiple times. This is a Miro/Channel web site issue and often rectifies itself overtime. """ % (videometadata[u'title'], videometadata[u'subtitle'])) return True # end updateMythVideo() def printStatistics(): global statistics # Print statistics sys.stdout.write(u""" -------------------Statistics-------------------- Number of Watch Recording's watched...... (% 5d) Number of Miro videos marked as seen..... (% 5d) Number of Miro videos deleted............ (% 5d) Number of New Miro videos downloaded..... (% 5d) Number of Miro/MythVideo's removed....... (% 5d) Number of Miro/MythVideo's added......... (% 5d) Number of Miro videos copies to MythVideo (% 5d) ------------------------------------------------- Total Unwatched Miro/Watch Recordings.... (% 5d) Total Miro/MythVideo videos to expire.... (% 5d) Total Miro/MythVideo videos.............. (% 5d) ------------------------------------------------- """ % (statistics[u'WR_watched'], statistics[u'Miro_marked_watch_seen'], statistics[u'Miro_videos_deleted'], statistics[u'Miros_videos_downloaded'], statistics[u'Miros_MythVideos_video_removed'], statistics[u'Miros_MythVideos_added'], statistics[u'Miros_MythVideos_copied'], statistics[u'Total_unwatched'], statistics[u'Total_Miro_expiring'], statistics[u'Total_Miro_MythVideos'], )) # end printStatistics() # Main script processing starts here def main(): """Support mirobridge from the command line returns True """ global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id, channel_num global flat, download_sleeptime, channel_watch_only, channel_mythvideo_only, channel_new_watch_copy global vid_graphics_dirs, imagemagick, statistics, requirements_are_met global graphic_suffix, graphic_path_suffix, graphic_name_suffix global mythcommflag_recordings, mythcommflag_videos global local_only, metadata global parser, opts, args if opts.examples: # Display example information sys.stdout.write(examples_txt+'\n') sys.exit(0) if opts.version: # Display program information sys.stdout.write(u"\nTitle: (%s); Version: description(%s); Author: (%s)\n%s\n" % ( __title__, __version__, __author__, __purpose__ )) sys.exit(0) if opts.testenv: test_environment = True else: test_environment = False # Verify that Miro is not currently running if isMiroRunning(): sys.exit(1) # Verify that only None or one of the mutually exclusive (-W), (-M) and (-N) options is being used x = 0 if opts.new_watch_copy: x+=1 if opts.watch_only: x+=1 if opts.mythvideo_only: x+=1 if opts.import_opml: x+=1 if x > 1: logger.critical(u"The (-W), (-M), (-N) and (-i) options are mutually exclusive, "\ u"so only one can be specified at a time.") sys.exit(1) # Set option related global variables simulation = opts.simulation verbose = opts.verbose if opts.hostname: # Override localhostname if the user specified an hostname localhostname = opts.hostname # Validate settings ## Video base directory and current version and revision numbers 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,)) logger.info(u'') # Verify Miro version sufficent and Video file configuration correct. if not os.path.isdir(base_video_dir): logger.critical(u"The Miro Videos directory (%s) does not exist." % str(base_video_dir)) if test_environment: requirements_are_met = False else: sys.exit(1) 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 "\ u"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 "\ u"4.0.2 or higher.") % miroConfiguration(prefs.APP_VERSION)) if test_environment: requirements_are_met = False else: sys.exit(1) # Verify that the import opml option can be used if opts.import_opml: 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 "\ u"(%s) is too old." % miroConfiguration(prefs.APP_VERSION)) if test_environment: requirements_are_met = False else: sys.exit(1) if not os.path.isfile(opts.import_opml): logger.critical(u"The OPML import file (%s) does not exist" % opts.import_opml) if test_environment: requirements_are_met = False else: sys.exit(1) if len(opts.import_opml) > 5: if not opts.import_opml[:-4] != '.opml': logger.critical(u"The OPML import file (%s) must had a file extention of '.opml'" % \ opts.import_opml) if test_environment: requirements_are_met = False else: sys.exit(1) else: logger.critical(u"The OPML import file (%s) must had a file extention of '.opml'" % \ opts.import_opml) if test_environment: requirements_are_met = False else: sys.exit(1) # Get storage groups if getStorageGroups() == False: logger.critical(u"Retrieving storage groups from the MythTV data base failed") if test_environment: requirements_are_met = False else: sys.exit(1) elif not u'default' in storagegroups.keys(): logger.critical(u"There must be a 'Default' storage group") if test_environment: requirements_are_met = False else: sys.exit(1) if opts.channel: channel = opts.channel.split(u':') if len(channel) != 2: logger.critical((u"The Channel (%s) must be in the format xxx:yyy with x and "\ u"y all numeric.") % str(opts.channel)) if test_environment: requirements_are_met = False else: sys.exit(1) elif not _can_int(channel[0]) or not _can_int(channel[1]): logger.critical(u"The Channel_id (%s) and Channel_num (%s) must be numeric." % \ (channel[0], channel[1])) if test_environment: requirements_are_met = False else: sys.exit(1) else: channel_id = int(channel[0]) channel_num = int(channel[1]) if opts.sleeptime: if not _can_int(opts.sleeptime): logger.critical(u"Auto-download sleep time (%s) must be numeric." % str(opts.sleeptime)) if test_environment: requirements_are_met = False else: sys.exit(1) else: download_sleeptime = float(opts.sleeptime) getMythtvDirectories() # Initialize all the Video and graphics directory dictionary if opts.nosubdirs: # Did the user want a flat MythVideo "Miro" directory structure? flat = True # Get the values in the mirobridge.conf configuration file setUseroptions() if opts.watch_only: # ALL Miro videos will only be viewed in the MythTV "Watch Recordings" screen channel_watch_only = [u'all'] if opts.mythvideo_only: # ALL Miro videos will be copied to MythVideo and removed from Miro channel_mythvideo_only = {u'all': vid_graphics_dirs[u'mythvideo']+u'Miro/'} # Once watched ALL Miro videos will be copied to MythVideo and removed from Miro if opts.new_watch_copy: channel_new_watch_copy = {u'all': vid_graphics_dirs[u'mythvideo']+u'Miro/'} # Verify that "Mythvideo Only" and "New-Watch-Copy" channels do not clash if len(channel_mythvideo_only) and len(channel_new_watch_copy): for key in channel_mythvideo_only.keys(): if key in channel_new_watch_copy.keys(): logger.critical((u'The Miro Channel (%s) cannot be used as both a "Mythvideo '\ u'Only" and "New-Watch-Copy" channel.') % key) if test_environment: requirements_are_met = False else: sys.exit(1) # Verify that ImageMagick is installed ret = useImageMagick(u"convert -version") if ret < 0 or ret > 1: logger.critical(u"ImageMagick must be installed, graphics cannot be resized or "\ u"converted to the required graphics format (e.g. jpg and or png)") if test_environment: requirements_are_met = False else: sys.exit(1) # Verify that the "DeletesFollowLinks" setting is not set to the character '1' if mythdb.settings.NULL.DeletesFollowLinks == '1': logger.critical(u'The MythTV back end setting "Follow symbolic links when deleting '\ u'files" is checked and it is incompatible with MiroBridge processing. '\ u'It must be unchecked it to use MiroBridge.\nTo uncheck this setting '\ u'start "mythtv-setup" or with Mythbuntu start "MythTV Backend Setup" '\ u'and then General->Miscellaneous Settings and uncheck the "Follow '\ u'symbolic links when deleting files" setting') if test_environment: requirements_are_met = False else: sys.exit(1) # Initialize class with the metadata methods metadata = MetaData(mythdb, Video, Record, storagegroups, vid_graphics_dirs, channel_id, ffmpeg, logger, simulation, verbose, ) if opts.testenv: # All tests passed metadata.getVideoDetails(u"") # Test that ffmpeg is available if ffmpeg and requirements_are_met: logger.info(u"The environment test passed !\n\n") sys.exit(0) else: logger.critical(u"The environment test FAILED. See previously displayed error messages!") sys.exit(1) if opts.addchannel != u'OFF': # Add a Miro Channel record - Should only be done once createChannelRecord(opts.addchannel, channel_id, channel_num) logger.info(u"The Miro Channel record has been successfully created !\n\n") sys.exit(0) ########################################### # Mainlogic for all Miro bridge and MythTV ########################################### # # 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(miroConfiguration(prefs.THEME_NAME)) # # Only required for Miro 4 if miroConfiguration(prefs.APP_VERSION)[0] == u"4": app.info_updater = InfoUpdater() app.cli_events = EventHandler() app.cli_events.connect_to_signals() # Only required for Miro 4 and higher if miroConfiguration(prefs.APP_VERSION) > u"4.0": 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: logger.critical( u"Starting Miro Frontend and Backend failed: (%s)\n(%s)" % \ (app.cli_events.startup_failure[0], app.cli_events.startup_failure[1])) app.controller.shutdown() time.sleep(5) # Let the shutdown processing complete sys.exit(1) # Only required for Miro 4 if miroConfiguration(prefs.APP_VERSION)[0] == u"4": app.movie_data_program_info = movie_data_program_info # Only required for Miro 4 and higher if miroConfiguration(prefs.APP_VERSION) > u"4.0": messages.FrontendStarted().send_to_backend() app.cli_interpreter = MiroInterpreter() if opts.verbose: app.cli_interpreter.verbose = True else: app.cli_interpreter.verbose = False app.cli_interpreter.simulation = opts.simulation app.cli_interpreter.videofiles = [] app.cli_interpreter.downloading = False app.cli_interpreter.icon_cache_dir = miroConfiguration( prefs.ICON_CACHE_DIRECTORY) app.cli_interpreter.imagemagick = imagemagick app.cli_interpreter.statistics = statistics # ## Version specific logic if miroConfiguration(prefs.APP_VERSION) < u"2.5.0": app.renderer = app.cli_interpreter elif miroConfiguration(prefs.APP_VERSION) > u"4.0": pass else: app.movie_data_program_info = \ app.cli_interpreter.movie_data_program_info # # Attempt to import an opml file # if opts.import_opml: results = 0 try: app.cli_interpreter.do_mythtv_import_opml(opts.import_opml) # Let the Miro backend process the OPML file before shutting down time.sleep(30) except Exception, e: logger.critical(u"Import of OPML file (%s) failed, error(%s)." % (opts.import_opml, e)) results = 1 # Gracefully close the Miro database and shutdown the Miro Front and Back ends app.controller.shutdown() time.sleep(5) # Let the shutdown processing complete sys.exit(results) # # Optionally Update Miro feeds and # download any "autodownloadable" videos which are pending # if not opts.no_autodownload: if opts.verbose: app.cli_interpreter.verbose = False app.cli_interpreter.do_mythtv_getunwatched(u'') before_download = len(app.cli_interpreter.videofiles) if opts.verbose: app.cli_interpreter.verbose = True if miroConfiguration(prefs.APP_VERSION) < u"4.0": # Miro 4 automatically refreshes feeds and downloads app.cli_interpreter.do_mythtv_update_autodownload(u'') time.sleep(download_sleeptime) firsttime = True while True: app.cli_interpreter.do_mythtv_check_downloading(u'') if app.cli_interpreter.downloading: time.sleep(30) firsttime = False continue elif firsttime: time.sleep(download_sleeptime) firsttime = False continue else: break if opts.verbose: app.cli_interpreter.verbose = False app.cli_interpreter.do_mythtv_getunwatched(u'') after_download = len(app.cli_interpreter.videofiles) statistics[u'Miros_videos_downloaded'] = after_download - before_download if opts.verbose: app.cli_interpreter.verbose = True # Deal with orphaned oldrecorded records. # These records indicate that the MythTV user deleted the video # from the Watched Recordings screen or from MythVideo # These video items must also be deleted from Miro videostodelete = getOldrecordedOrphans() if len(videostodelete): displayMessage( u"Starting Miro delete of videos deleted in the MythTV "\ u"Watched Recordings screen.") for video in videostodelete: # Completely remove the video and item information from Miro app.cli_interpreter.do_mythtv_item_remove( [video[u'title'], video[u'subtitle']]) # # Collect the set of played Miro video files # app.cli_interpreter.videofiles = getPlayedMiroVideos() # # Updated the played status of items # if app.cli_interpreter.videofiles: displayMessage(u"Starting Miro update of watched MythTV videos") app.cli_interpreter.do_mythtv_updatewatched(u'') # # Get the unwatched videos details from Miro # app.cli_interpreter.do_mythtv_getunwatched(u'') unwatched = app.cli_interpreter.videofiles # # Get the watched videos details from Miro # app.cli_interpreter.do_mythtv_getwatched(u'') watched = app.cli_interpreter.videofiles # # Massage empty titles and subtitles from Miro # for item in unwatched: # Deal with empty titles and subtitles from Miro if not item[u'channelTitle']: item[u'channelTitle'] = emptyTitle if not item[u'title']: item[u'title'] = emptySubTitle for item in watched: # Deal with empty titles and subtitles from Miro if not item[u'channelTitle']: item[u'channelTitle'] = emptyTitle if not item[u'title']: item[u'title'] = emptySubTitle # # Remove any duplicate Miro videoes from the unwatched or watched list of Miro videos # This means that Miro has duplicates due to a Miro/Channel website issue # These videos should not be added to the MythTV Watch Recordings screen # unwatched_copy = [] for item in unwatched: unwatched_copy.append(item) for item in unwatched_copy: # Check for a duplicate against already watched Miro videos for x in watched: if item[u'channelTitle'] == x[u'channelTitle'] and \ item[u'title'] == x[u'title']: try: unwatched.remove(item) # Completely remove this duplicate video and item information from Miro app.cli_interpreter.do_mythtv_item_remove( item[u'videoFilename']) displayMessage(( u'''Skipped adding a duplicate Miro video to the MythTV Watch Recordings screen (%s - %s) which is already in MythVideo. Sometimes a Miro channel has the same video downloaded multiple times. This is a Miro/Channel web site issue and often rectifies itself overtime.''') % (item[u'channelTitle'], item[u'title'])) except ValueError: pass duplicates = [] for item in unwatched_copy: dup_flag = 0 for x in unwatched: # Check for a duplicate against un-watched Miro videos if item[u'channelTitle'] == x[u'channelTitle'] and item[u'title'] == x[u'title']: dup_flag+=1 if dup_flag > 1: for x in duplicates: if item[u'channelTitle'] == x[u'channelTitle'] and item[u'title'] == x[u'title']: break else: duplicates.append(item) for duplicate in duplicates: try: unwatched.remove(duplicate) # Completely remove this duplicate video and item information from Miro app.cli_interpreter.do_mythtv_item_remove(duplicate[u'videoFilename']) displayMessage((u"Skipped adding a Miro video to the MythTV Watch Recordings "\ u"screen (%s - %s) as there are duplicate 'new' video items."\ u"\nSometimes a Miro channel has the same video downloaded "\ u"multiple times.\nThis is a Miro/Channel web site issue and "\ u"often rectifies itself overtime.") % \ (duplicate[u'channelTitle'], duplicate[u'title'])) except ValueError: pass # # Deal with any Channel videos that are to be copied and removed from Miro # copy_items = [] # Copy unwatched and watched Miro videos (all or only selected Channels) if u'all' in channel_mythvideo_only.keys(): for array in [watched, unwatched]: for item in array: copy_items.append(item) elif len(channel_mythvideo_only): for array in [watched, unwatched]: for video in array: if filter(is_not_punct_char, video[u'channelTitle'].lower()) in \ channel_mythvideo_only.keys(): copy_items.append(video) # Copy ONLY watched Miro videos (all or only selected Channels) if u'all' in channel_new_watch_copy.keys(): for video in watched: copy_items.append(video) elif len(channel_new_watch_copy): for video in watched: if filter(is_not_punct_char, video[u'channelTitle'].lower()) in \ channel_new_watch_copy.keys(): copy_items.append(video) channels_to_copy = {} for key in channel_mythvideo_only.keys(): channels_to_copy[key] = channel_mythvideo_only[key] for key in channel_new_watch_copy.keys(): channels_to_copy[key] = channel_new_watch_copy[key] for video in copy_items: if channels_to_copy.has_key('all'): copy_dir = u"%s%s/" % (channels_to_copy['all'], sanitiseFileName(video[u'channelTitle'])) else: copy_dir = channels_to_copy[filter(is_not_punct_char, video[u'channelTitle'].lower())] # Create the subdirectories to copy the video into if not os.path.isdir(copy_dir): if simulation: logger.info(u"Simulation: Creating the MythVideo directory (%s)." % (copy_dir)) else: os.makedirs(copy_dir) # Copy the Miro video file # This filename is needed later for deleting in Miro save_video_filename = video[u'videoFilename'] ext = getExtention(video[u'videoFilename']) if ext.lower() == u'm4v': ext = u'mpg' filepath = u"%s%s - %s.%s" % (copy_dir, sanitiseFileName(video[u'channelTitle']), sanitiseFileName(video[u'title']), ext) if simulation: logger.info(u"Simulation: Copying the Miro video (%s) to the MythVideo directory (%s)." % \ (video[u'videoFilename'], filepath)) else: try: # Miro video copied into a MythVideo directory shutil.copy2(video[u'videoFilename'], filepath) statistics[u'Miros_MythVideos_copied']+=1 if u'mythvideo' in storagegroups.keys() and not local_only: video[u'videoFilename'] = filepath.replace(storagegroups[u'mythvideo'], u'') else: video[u'videoFilename'] = filepath except Exception, e: logger.critical((u"Copying the Miro video (%s) to the MythVideo directory (%s)."\ u"\n This maybe a permissions error (mirobridge.py does "\ u"not have permission to write to the directory), error(%s)") % \ (video[u'videoFilename'], filepath, e)) # Gracefully close the Miro database and shutdown the Miro Front and Back ends app.controller.shutdown() time.sleep(5) # Let the shutdown processing complete sys.exit(1) # Copy the Channel or item's icon if video[u'channel_icon'] and not video[u'channelTitle'].lower() in channel_icon_override: pass else: if video[u'item_icon']: video[u'channel_icon'] = video[u'item_icon'] # Get any graphics that already exist graphics = metadata.getMetadata(sanitiseFileName(video[u'channelTitle'])) if video[u'channel_icon'] and graphics['coverart'] == u'': ext = getExtention(video[u'channel_icon']) if video[u'channelTitle'].lower() in channel_icon_override: filepath = u"%s%s - %s%s.%s" % (vid_graphics_dirs[u'posterdir'], sanitiseFileName(video[u'channelTitle']), sanitiseFileName(video[u'title']), graphic_suffix[u'posterdir'], ext) else: filepath = u"%s%s%s.%s" % (vid_graphics_dirs[u'posterdir'], sanitiseFileName(video[u'channelTitle']), graphic_suffix[u'posterdir'], ext) # There may already be a Channel icon available # or it is a symlink which needs to be replaced if not os.path.isfile(filepath) or os.path.islink(filepath): if simulation: logger.info((u"Simulation: Copying the Channel Icon (%s) to the poster "\ u"directory (%s).") % (video[u'channel_icon'], filepath)) else: try: # Miro Channel icon copied into a MythVideo directory try: # Remove any old symlink file os.remove(filepath) except OSError: pass shutil.copy2(video[u'channel_icon'], filepath) if u'posterdir' in storagegroups.keys() and not local_only: video[u'channel_icon'] = filepath.replace(storagegroups[u'posterdir'], u'') else: video[u'channel_icon'] = filepath except Exception, e: logger.critical((u"Copying the Channel Icon (%s) to the poster directory "\ u"(%s).\n This maybe a permissions error "\ u"(mirobridge.py does not have permission to write to the "\ u"directory), error(%s)") % \ (video[u'channel_icon'], filepath, e)) # Gracefully close the Miro database and shutdown the Miro Front and Back ends app.controller.shutdown() time.sleep(5) # Let the shutdown processing complete sys.exit(1) else: if u'posterdir' in storagegroups.keys() and not local_only: video[u'channel_icon'] = filepath.replace(storagegroups[u'posterdir'], u'') else: video[u'channel_icon'] = filepath else: video[u'channel_icon'] = graphics['coverart'] # There may already be a Screenshot available or it is a symlink which needs to be replaced if video[u'screenshot']: ext = getExtention(video[u'screenshot']) filepath = u"%s%s - %s%s.%s" % (vid_graphics_dirs[u'episodeimagedir'], sanitiseFileName(video[u'channelTitle']), sanitiseFileName(video[u'title']), graphic_suffix[u'episodeimagedir'], ext) else: filepath = u'' if not os.path.isfile(filepath) or os.path.islink(filepath): if video[u'screenshot']: if simulation: logger.info((u"Simulation: Copying the Screenshot (%s) to the Screenshot "\ u"directory (%s).") % (video[u'screenshot'], filepath)) else: try: # Miro Channel icon copied into a MythVideo directory try: # Remove any old symlink file os.remove(filepath) except OSError: pass shutil.copy2(video[u'screenshot'], filepath) displayMessage(u"Copied Miro screenshot file (%s) to MythVideo (%s)" % \ (video[u'screenshot'], filepath)) if u'episodeimagedir' in storagegroups.keys() and not local_only: video[u'screenshot'] = filepath.replace(storagegroups[u'episodeimagedir'], u'') else: video[u'screenshot'] = filepath except Exception, e: logger.critical((u"Copying the Screenshot (%s) to the Screenshot directory "\ u"(%s).\n This maybe a permissions error "\ u"(mirobridge.py does not have permission to write to the "\ u"directory), error(%s)") % \ (video[u'screenshot'], filepath, e)) # Gracefully close the Miro database and shutdown the Miro Front and Back ends app.controller.shutdown() time.sleep(5) # Let the shutdown processing complete sys.exit(1) elif video[u'screenshot']: if u'episodeimagedir' in storagegroups.keys() and not local_only: video[u'screenshot'] = filepath.replace(storagegroups[u'episodeimagedir'], u'') else: video[u'screenshot'] = filepath video[u'copied'] = True # Mark this video item as being copied # Completely remove the video and item information from Miro app.cli_interpreter.do_mythtv_item_remove(save_video_filename) # Gracefully close the Miro database and shutdown the Miro Front and Back ends app.controller.shutdown() time.sleep(5) # Let the shutdown processing complete # # Add and delete MythTV (Watch Recordings) Miro recorded records # Add and remove symlinks for Miro video files # # Check if the user does not want any channels Added to the "Watch Recordings" screen if channel_mythvideo_only.has_key(u'all'): for video in unwatched: watched.append(video) unwatched = [] else: if len(channel_mythvideo_only): unwatched_copy = [] for video in unwatched: if not filter(is_not_punct_char, video[u'channelTitle'].lower()) in \ channel_mythvideo_only.keys(): unwatched_copy.append(video) else: watched.append(video) unwatched = unwatched_copy statistics[u'Total_unwatched'] = len(unwatched) if not len(unwatched): displayMessage(u"There are no Miro unwatched video items to add as MythTV Recorded videos.") if not updateMythRecorded(unwatched): logger.critical(u"Updating MythTV Recording with Miro video files failed." % \ str(base_video_dir)) sys.exit(1) # # Add and delete MythVideo records for played Miro Videos # Add and delete symbolic links to Miro Videos and subdirectories # Add and delete symbolic links to coverart/Miro icons and Miro screenshots/fanart # if len(channel_watch_only): # If the user does not want any channels moved to MythVideo exit if channel_watch_only[0].lower() == u'all': printStatistics() return True if not len(watched): displayMessage(u"There are no Miro watched items to add to MythVideo") if not updateMythVideo(watched): logger.critical(u"Updating MythVideo with Miro video files failed.") sys.exit(1) printStatistics() return True # end main if __name__ == "__main__": myapp = singleinstance(u'/tmp/mirobridge.pid') # # check is another instance of Miro Bridge running # if myapp.alreadyrunning(): print u'\nMiro Bridge is already running only one instance can run at a time\n\n' sys.exit(0) main() displayMessage(u"Miro Bridge Processing completed")