MythTV  0.27pre
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Groups Pages
mirobridge.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3 # ----------------------
4 # Name: mirobridge.py Maintains MythTV database with Miro's downloaded video files.
5 # Python Script
6 # Author: R.D. Vaughan
7 # Purpose: This python script is intended to perform synchronise Miro's video files with MythTV's
8 # "Watch Recordings" and MythVideo.
9 #
10 # The source of all video files is from those downloaded my Miro.
11 # The source of all cover art and screen shoots are from those downloaded and maintained by
12 # Miro.
13 # Miro v2.03 or later must be already installed and configured and already capable of
14 # downloading videos.
15 #
16 # Command line examples:
17 # See help (-u and -h) options
18 #
19 # License:Creative Commons GNU GPL v2
20 # (http://creativecommons.org/licenses/GPL/2.0/)
21 #-------------------------------------
22 __title__ ="mirobridge - Maintains Miro's Video files with MythTV";
23 __author__="R.D.Vaughan"
24 __purpose__='''
25 This python script is intended to synchronise Miro's video files with MythTV's "Watch Recordings" and MythVideo.
26 
27 The source of all video files are from those downloaded my Miro.
28 The source of all meta data for the video files is from the Miro data base.
29 The source of all cover art and screen shots are from those downloaded and maintained by Miro.
30 Miro v2.0.3 or later must already be installed and configured and capable of downloading videos.
31 '''
32 
33 __version__=u"v0.7.0"
34 # 0.1.0 Initial development
35 # 0.2.0 Initial Alpha release for internal testing only
36 # 0.2.1 Fixes from initial alpha test
37 # Renamed imported micro python modules
38 # 0.2.2 Fixed the duplicate video situation when Miro and a Channel feed has an issue which causes
39 # the same video to be downloaded multiple time. This situation is now detected and protects
40 # the duplicates from being added to either the MythTV or MythVideo.
41 # Fixes a few problems with stripping HTML and converting HTML characters in Miro descriptions.
42 # Changed the identification of HD and 720 videos to conform to MythTV standards
43 # Removed the file "mirobridge_util.py" from the mirobridge distribution as the main Miro
44 # distribution file "util.py" is used instead.
45 # Changed ALL symlink Miro icons (coverart) to a copied Miro icon file, replace any coverart that
46 # is currently a symlink to a copied Miro icon file and check that each MythVideo subdirectory
47 # has an actual file that has the proper naming convention that supports storage groups.
48 # Use using ImageMagick's utility 'mogrify' to convert Miro's screenshots to true png's (they are
49 # really jpg files with a png extension) and reduce their size by 50% for the Watch Recordings
50 # screen. Imagemagick is now a requirement to have installed.
51 # Fixed a bug when either the Miro video's Channel title or title exceeded MythTV database 128
52 # characters limit for their equivalent title and subtitle fields.
53 # When Miro has no screen shot for a Video substitute the item icon if it is high enough quality.
54 # 0.2.3 All subdirectory coverfiles names are changed to "folder".(jpg/png) and gif files are converted.
55 # Imagemagick is now mandatory as Miro has cover art which are gif types and must be converted
56 # to either jpg or png so they will be recognised as folder coverart for Storage Groups.
57 # Added if coverart from Miro is a gif type convert it to a jpg.
58 # Removed resizing screenshots by 50% when adding a screen shot to the Watch Recordings screen. As
59 # some graphics were already very small.
60 # Fixed a bug were a Watch Recordings video was deleted by the user but the graphics files were not
61 # also deleted.
62 # Converted all mirobridge console messages to proper logger format (critical, error, warning, info)
63 # Add changes as Anduin did to ttvdb.py to force 'utf8' output
64 # Tested foreign language video's with their foreign language metadata - No problem
65 # 0.2.4 Added a percentage downloaded message for each item that is downloading - updated every 30 secs.
66 # Made Miro update and auto-download the default (no option -d). Added option (-n) to suppress
67 # update and download processing.
68 # If there is no screenshot then create one with ffmpeg. The size for Watch Recordings is 320 wide
69 # and the mythtvideo screenshots are the same size as the video. From this point on this feature
70 # will known as the "iamlindoro effect".
71 # Fixed a bug where fold covers were being created even if a "folder.png" was already available.
72 # As Anduin had done with ttvdb's support *.py files mirobridge's support *.py and example conf
73 # files have been moved to a mirobridge subdirectory.
74 # Added a check that makes sure that the Miro items are only video files. Audio files are skipped.
75 # This is the final version that will support Miro v2.0.3 all higher versions will support
76 # Miro v2.5.2 and higher. Except for small bugs this version will no longer be enhanced.
77 # Added a small statistics report.
78 # 0.2.5 Changes required for mirobridge to support Miro v2.5.2 have been made now mirobridge dynamically
79 # supports both versions v2.0.3 and v2.5.2
80 # Fixed a statistics report bug for the copied to MythVideo totals and add new totals.
81 # Fixed a bug where the Miro screen shot was not being resized for the Watch Recordings screen.
82 # Fixed a bug when the Miro download time has not been set. In that case use the current date and
83 # time. If this is not done the video cannot be deleted from Watch Recordings.
84 # Fixed a bug where a video copied to MythVideo gets stranded in the Watch Recordings screen
85 # even though the video file has been deleted in Miro.
86 # Fixed a bug where a video that has been watched does not get removed from the Watch Recordings
87 # screen. This stranded in the Watch Recordings screen even though the video file has been
88 # deleted in Miro. This only happened when videos had been watched BUT there were no new videos
89 # to add to the Watch Recordings screen.
90 # Fixed a typo in the statistics report.
91 # Fixed a Miro version check bug
92 # Fixed a test environment option bug
93 # Changed the "except" statement on imports to "except Exception:" as Anduin did to ttvdb.py
94 # 0.3.0 Beta release
95 # 0.3.1 Changed the check for Imagmagick convert utility and ffmpeg as the positive return value
96 # is different on some distributions (0 or 1). The fail value is consistent (127).
97 # Fixed the environment test option (-t) at times could give incorrect success message
98 # 0.3.2 Fixed a bug when an empty item description would abort the script on an "extras" request
99 # Fixed a bug when the Watched item processing failed message would abort the script
100 # Fixed a bug when a user accidental deletes a video file symlink then the video symlink is NOT
101 # recreated as it should have been.
102 # Fixed a bug where double quotes in the title or subtitle caused issues with file names of graphics
103 # Added to the statistics report the total number of Miro-MythVideo videos that will eventually be
104 # expired and removed by Miro.
105 # Added a info log message with the Miro Bridge version number, which may help in problem analysis.
106 # Added trapping and diagnostic messages when the HTML tags could not be removed from a description.
107 # Added a check that the installed "pyparsing" python library is at least v1.5.0
108 # Added detection of a Miro video deletion though the MythVideo UI. When this occurs
109 # Miro is told to also deletes the video, graphics and meta data.
110 # 0.3.3 Status change from Beta to production. No code changes
111 # 0.3.4 Fixed when checking for orphaned videometadata records and there were no Miro videometadata
112 # records.
113 # Added additional detection and restrictions to the supported versions of Miro (minimum v2.0.3)
114 # preferrably v2.5.2 or higher.
115 # 0.3.5 Use the MythVideo.py binding rmMetadata() routine to delete old videometatdata records.
116 # Added access checks for all directories that Miro Bridge needs to write
117 # 0.3.6 Modifications as per requested changes.
118 # 0.3.7 Fixed a bug with previous modifications that impacted Miro v2.0.3 only
119 # 0.3.8 Fixed unicode errors with file names
120 # Change to assist SG image hunting added the suffix "_coverart, _fanart, _banner,
121 # _screenshot" respectively for any copied/created graphics.
122 # 0.3.9 Fixed an issue when deleting a Miro video and the title/subtitle was not found due to special
123 # characters. The search and matching is now more robust.
124 # Fixed a bug where file name unicode errors caused an abort when creating screen shots
125 # Removed from the mirobrodge.conf file the sections "[tv] and [movies]". This functionality
126 # will be added to the Jamu v0.5.0 -MW option.
127 # Added a check for locally available banners and fanart files when creating a MythVideo record.
128 # This is added as Jamu v0.5.0 option -MW downloads graphics from TVDB and TMDB for Miro videos
129 # when available.
130 # Modified the check for mirobridge.conf to accomodate the needs of Mythbuntu.
131 # Add mythcommflag seektable building for both recordings and mythvideo Miro videos.
132 # 0.4.0 Fixed an abort where a variable had not been named properly due to a cut and paste error.
133 # 0.4.1 Added a check that no other instance of Miro Bridge or Miro is already running. If there is
134 # then post a critical error message and exit.
135 # Do not add the Miro Bridge default banner when a Miro video has no subtitle as it overlaps
136 # the title display on MythVideo information pop-ups.
137 # Fixed a bug where a folder icon was being recreated even when it already existed.
138 # 0.4.2 Added a missing import of "htmlentitydefs" which is rarely used in the description/plot XTML parsing
139 # 0.4.3 Added support for audio type detection (2-channel, 6-channel, ...) that matches changes in ffmpeg.
140 # ffmpeg is used in the detection of a video file's audio properties.
141 # New Miro Videos added to the Default recordings directory have their names conform to MythTV standards
142 # of "CHANID_ISODATETIME.ext". This resolves an obsure bug that caused orphaned screen shot graphics and
143 # issues with Miro video deletions from the Watch Recordings screen if MiroBridge was run when the user
144 # had MythTV started and in the Watch Recordings screen.
145 # 0.4.4 Fixed a unicode issue with data read from a subprocess call.
146 # Fixed an issue with the check for other instances of mirobridge.py running.
147 # 0.4.5 Fixed a deletion issue when a Miro video subtitle contained more than 128 characters.
148 # Disabled seek table creation as a number of the Miro video types (e.g. mov) do not work in MythTV with
149 # seek tables.
150 # 0.4.6 Changed "original air date" and "air date" to be Miro's item release date. This is more appropriate then
151 # using download date as was done previously. Download date is still the fall back of there is no
152 # release date.
153 # 0.4.7 Changed all occurances of "strftime(u'" to "strftime('" as the unicode causes issues with python versions
154 # less than 2.6
155 # 0.4.8 Some Miro "release" date values are not valid. Override with the current date.
156 # 0.4.9 The ffmpeg SVN (e.g. SVN-r20151) is now outputting additional metadata, skip metadata that cannot be
157 # processed.
158 # 0.5.0 Correct the addition of adding hostnames to videometadata records and the use of relative paths when
159 # there is no Videos Storage Group.
160 # Added more informative error messages when trying to connect to the MythTV data base
161 # 0.5.1 Fixed the config "all" options for command line -N and -M and config file sections [watch_then_copy]
162 # and [mythvideo_only].
163 # Changed return codes from True to 0 and False to 1.
164 # Added display of the directories that will be used by MiroBridge and whether they are storage groups.
165 # Added file name sanitising logic plus a config file variable to add characters to be replaced by '_'.
166 # The config file variable 'file name_char_filter' is required by users who save MiroBridge files on a
167 # file system which only supports MS-Windows file naming conventions.
168 # 0.5.2 Convert to new python bindings and replace all direct mysql data base calls. See ticket #7264
169 # Remove the requirement for the MySQLdb python library.
170 # Remove all seek table processing as it is not used due to issues with some video file types. This code had been
171 # previously disable but the code and related mythcommflag related code has been removed entirely.
172 # Initialized new videometadata fields 'releasedate' and 'hash'.
173 # 0.5.3 Fixed Exception messages
174 # 0.5.4 Add the command line option (-i) to import an OPML file that was exported from Miro on a different PC.
175 # This new option allows configuring Miro channels on a separate PC then importing those channels on
176 # a Myth backend without a keyboard. No Miro GUI needs to run on the MythTV backend.
177 # 0.5.5 Fixed bug #8051 - creating hash value for non-SG Miro video files had an incorrect variable and missing
178 # library import.
179 # 0.5.6 Fixed an abort and subsequent process hang when displaying a critical Miro start-up error
180 # 0.5.7 The "DeletesFollowLinks" setting is incompatible with MiroBridge processings. A check
181 # and termination of MiroBridge with an appropriate error message has been added.
182 # Added better system error messages when an IOError exception occurs
183 # 0.5.8 Add support for Miro version 3.0
184 # 0.5.9 Update for changes in Python Bindings
185 # 0.6.0 Fixed a issue when a Miro video's metadata does not have a title. It was being re-added and the
186 # database title fields in several records was being left empty.
187 # 0.6.1 Modifications to support MythTV python bindings changes
188 # 0.6.2 Trapped possible unicode errors which would hang the MiroBridge process
189 # 0.6.3 Pull hostname from python bindings instead of socket libraries
190 # 0.6.4 MythTV python bindings changes
191 # 0.6.5 Added support for Miro v3.5.x
192 # Small internal document changes
193 # 0.6.6 Fixed screenshot code due to changes in ffmpeg. First
194 # noticed in Ubuntu 10.10 (ffmepg v 0.6-4:0.6-2ubuntu6)
195 # 0.6.7 Added support for Miro v4.0.2 or higher
196 # Integrate with the new metadata functionality in recordings. Now users can specify graphics for
197 # Miro Channels or set the Miro Channel name to match an entry in ttvdb.com and MythTV will
198 # download the artwork automatically.
199 # Automatically convert any copied Miro videos with an inetref of '99999999' which was only required
200 # for the defunct Jamu script. The category is changed to 'Video Cast" and the inetref is removed
201 # unless there is a matching Recording rule.
202 # Silenced verbose output from ffmpeg when creating a screenshot
203 # Fixed delOldRecording abort when a Channel sends two videos with identical published date and time.
204 # All starttimes are now unique.
205 # Fixed aborts caused by bad metadata in Miro (videoFilename)
206 # Fixed a minor bug when a video is marked as watched within Miro but was not being removed from
207 # "Watched Recordings" until the video expired
208 # Removed creation of "folder.png" graphics when creating directories as that is no longer used
209 # by MythVideo
210 # Fixed the options "-h, --help" command line display
211 # 0.6.8 Sometimes Miro metadata has no video filename. Skip these invalid videos.
212 # 0.6.9 Adjust to datetime issues with MythTV v0.26's move to UTC datatimes in DB
213 # 0.7.0 Fix bug introduced with v0.6.9, ticket reported as #11219 and #11220
214 
215 examples_txt=u'''
216 For examples, please see the Mirobridge's wiki page at http://www.mythtv.org/wiki/MiroBridge
217 '''
218 
219 # Common function imports
220 import sys, os, re, locale, subprocess, locale, ConfigParser, codecs, shutil, struct
221 import fnmatch, string, time, logging, traceback, platform, fnmatch, ConfigParser
222 from datetime import timedelta
223 from optparse import OptionParser
224 from socket import gethostbyname
225 import formatter
226 import htmlentitydefs
227 
228 # Set command line options and arguments
229 parser = OptionParser(usage=u"%prog usage: mirobridge -huevstdociVHSCWM [parameters]\n")
230 
231 parser.add_option( "-e", "--examples", action="store_true", default=False, dest="examples",
232  help=u"Display examples for executing the mirobridge script")
233 parser.add_option( "-v", "--version", action="store_true", default=False, dest="version",
234  help=u"Display version and author information")
235 parser.add_option( "-s", "--simulation", action="store_true", default=False, dest="simulation",
236  help=u"Simulation (dry run), no files are copied, symlinks created or MythTV data "\
237  u"bases altered. If option (-n) is NOT specified Miro auto downloads WILL take "\
238  u"place. See option (-n) help for details.")
239 parser.add_option( "-t", "--testenv", action="store_true", default=False, dest="testenv",
240  help=u"Test that the local environment can run all mirobridge functionality")
241 parser.add_option( "-n", "--no_autodownload", action="store_true", default=False, dest="no_autodownload",
242  help=u"Do not perform Miro Channel updates, video expiry and auto-downloadings. "\
243  u"Default is to perform all perform all Channel maintenance features.")
244 parser.add_option( "-o", "--nosubdirs", action="store_true", default=False, dest="nosubdirs",
245  help=u"Organise MythVideo's Miro directory WITHOUT Miro channel subdirectories. "\
246  u"The default is to have Channel subdirectories.")
247 parser.add_option( "-c", "--channel", metavar="CHANNEL_ID:CHANNEL_NUM", default="", dest="channel",
248  help=u'Specifies the channel id that is used for Miros unplayed recordings. Enter '\
249  u'as "xxxx:yyy". Default is 9999:999. Be warned that once you change the '\
250  u'default channel_id "9999" you must always use this option!')
251 parser.add_option( "-i", "--import_opml", metavar="OPMLFILEPATH", default="", dest="import_opml",
252  help=u'Import Miro exported OPML file containing Channel configurations. File '\
253  u'name must be a fully qualified path. This option is exclusive to Miro '\
254  u'v2.5.x and higher.')
255 parser.add_option( "-V", "--verbose", action="store_true", default=False, dest="verbose",
256  help=u"Display verbose messages when processing")
257 parser.add_option( "-H", "--hostname", metavar="HOSTNAME", default="", dest="hostname",
258  help=u"MythTV Backend hostname mirobridge is to up date")
259 parser.add_option( "-S", "--sleeptime", metavar="SLEEP_DELAY_SECONDS", default="", dest="sleeptime",
260  help=u"The amount of seconds to wait for an auto download to start.\nThe default "\
261  u"is 60 seconds, but this may need to be adjusted for slower Internet connections.")
262 parser.add_option( "-C", "--addchannel", metavar="ICONFILE_PATH", default="OFF", dest="addchannel",
263  help=u'Add a Miro Channel record to MythTV. This gets rid of the "#9999 #9999" '\
264  u'on the Watch Recordings screen and replaces it with the usual\nthe channel '\
265  u'number and channel name.\nThe default if not overridden by the (-c) option '\
266  u'is channel number 999.\nIf a filename and path is supplied it will be set '\
267  u'as the channels icon. Make sure your override channel number is NOT one of '\
268  u'your current MythTV channel numbers.\nThis option is typically only used '\
269  u'once as there can only be one Miro channel record at a time.')
270 parser.add_option( "-N", "--new_watch_copy", action="store_true", default=False, dest="new_watch_copy",
271  help=u'For ALL Miro Channels: Use the "Watch Recording" screen to watch new Miro '\
272  u'downloads then once watched copy the videos, icons, screen shot and metadata '\
273  u'to MythVideo. Once coping is complete delete the video from Miro.\nThis option '\
274  u'overrides any "mirobridge.conf" settings.')
275 parser.add_option( "-W", "--watch_only", action="store_true", default=False, dest="watch_only",
276  help=u'For ALL Miro Channels: Only use "Watch Recording" never move any Miro videos '\
277  u'to MythVideo.\nThis option overrides any "mirobridge.conf" settings.')
278 parser.add_option( "-M", "--mythvideo_only", action="store_true", default=False, dest="mythvideo_only",
279  help=u'For ALL Miro Channel videos: Copy newly downloaded Miro videos to MythVideo '\
280  u'and removed from Miro. These Miro videos never appear in the MythTV "Watch '\
281  u'Recording" screen.\nThis option overrides any "mirobridge.conf" settings.')
282 
283 
284 # Global variables
285 opts, args = parser.parse_args() # Command line arguments and options
286 local_only = True
287 dir_dict = {u'posterdir': u"VideoArtworkDir", u'bannerdir': u'mythvideo.bannerDir',
288  u'fanartdir': u'mythvideo.fanartDir', u'episodeimagedir': u'mythvideo.screenshotDir',
289  u'mythvideo': u'VideoStartupDir'}
290 vid_graphics_dirs = {u'default' : u'', u'mythvideo': u'', u'posterdir' : u'',
291  u'bannerdir': u'', u'fanartdir': u'', u'episodeimagedir': u'',}
292 key_trans = {u'filename' : u'mythvideo', u'coverfile': u'posterdir',
293  u'banner' : u'bannerdir', u'fanart' : u'fanartdir',
294  u'screenshot': u'episodeimagedir'}
295 
296 graphic_suffix = {u'default' : u'', u'mythvideo': u'', u'posterdir': u'_coverart',
297  u'bannerdir': u'_banner', u'fanartdir': u'_fanart', u'episodeimagedir': u'_screenshot',}
298 graphic_path_suffix = u"%s%s%s.%s"
299 graphic_name_suffix = u"%s%s.%s"
300 
301 storagegroupnames = {u'Default' : u'default', u'Videos' : u'mythvideo',
302  u'Coverart': u'posterdir', u'Banners' : u'bannerdir',
303  u'Fanart' : u'fanartdir', u'Screenshots': u'episodeimagedir'}
304 storagegroups={} # The gobal dictionary is only populated with the current hosts storage group entries
305 image_extensions = [u"png", u"jpg", u"bmp"]
306 simulation = False
307 verbose = False
308 ffmpeg = True
309 channel_id = 9999
310 channel_num = 999
311 symlink_filename_format = u"%s - %s"
312 flat = False # The MythVideo Miro directory structure flat or channel subdirectories
313 download_sleeptime = float(60) # The default value for the time to wait for auto downloading to start
314 channel_icon_override = []
315 channel_watch_only = []
316 channel_mythvideo_only = {}
317 channel_new_watch_copy = {}
318 tv_channels = {}
319 movie_trailers = []
320 test_environment = False
321 requirements_are_met = True # Used with the (-t) test environment option
322 imagemagick = True
323 mythcommflag_recordings = u'%s -c %%s -s "%%s" --rebuild' # or u'mythcommflag -f "%s" --rebuild'
324 mythcommflag_videos = u'%s --rebuild --video "%%s"'
325 filename_char_filter = u"/%\000"
326 emptyTitle = u'_NO_TITLE_From_Miro'
327 emptySubTitle = u'_NO_SUBTITLE_From_Miro'
328 
329 
330 # Initalize Report Statistics:
331 statistics = {u'WR_watched': 0, u'Miro_marked_watch_seen': 0,
332  u'Miro_videos_deleted': 0, u'Miros_videos_downloaded': 0,
333  u'Miros_MythVideos_video_removed': 0, u'Miros_MythVideos_added': 0,
334  u'Miros_MythVideos_copied': 0, u'Total_unwatched': 0,
335  u'Total_Miro_expiring': 0, u'Total_Miro_MythVideos': 0}
336 
337 
338 class OutStreamEncoder(object):
339  """Wraps a stream with an encoder
340  """
341  def __init__(self, outstream, encoding=None):
342  self.out = outstream
343  if not encoding:
344  self.encoding = sys.getfilesystemencoding()
345  else:
346  self.encoding = encoding
347 
348  def write(self, obj):
349  """Wraps the output stream, encoding Unicode strings with the specified encoding"""
350  if isinstance(obj, unicode):
351  self.out.write(obj.encode(self.encoding))
352  else:
353  self.out.write(obj)
354 
355  def __getattr__(self, attr):
356  """Delegate everything but write to the stream"""
357  return getattr(self.out, attr)
358 # Sub class sys.stdout and sys.stderr as a utf8 stream. Deals with print and stdout unicode issues
359 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
360 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
361 
362 # Create logger
363 logger = logging.getLogger(u"mirobridge")
364 logger.setLevel(logging.DEBUG)
365 # Create console handler and set level to debug
366 ch = logging.StreamHandler()
367 ch.setLevel(logging.DEBUG)
368 # Create formatter
369 formatter = logging.Formatter(u"%(asctime)s - %(name)s - %(levelname)s - %(message)s")
370 # Add formatter to ch
371 ch.setFormatter(formatter)
372 # Add ch to logger
373 logger.addHandler(ch)
374 
375 # The pyparsing python library must be installed version 1.5.0 or higher
376 try:
377  from pyparsing import *
378  import pyparsing
379  if pyparsing.__version__ < "1.5.0":
380  logger.critical(u"The python library 'pyparsing' must be at version 1.5.0 or higher. "\
381  u"Your version is v%s" % pyparsing.__version__)
382  sys.exit(1)
383 except Exception, e:
384  logger.critical(u"The python library 'pyparsing' must be installed and be version 1.5.0 or "\
385  u"higher, error(%s)" % e)
386  sys.exit(1)
387 logger.info(u"Using python library 'pyparsing' version %s" % pyparsing.__version__)
388 
389 
390 # Find out if the MythTV python bindings can be accessed and instances can be created
391 try:
392  #If the MythTV python interface is found, we can insert data directly to MythDB or
393  #get the directories to store poster, fanart, banner and episode graphics.
394  from MythTV import OldRecorded, Recorded, RecordedProgram, Record, Channel, \
395  MythDB, Video, MythVideo, MythBE, MythError, MythLog
396  from MythTV.database import DBDataWrite
397  from MythTV.utility import datetime
398  mythdb = None
399  mythbeconn = None
400  try:
401  #Create an instance of each: MythDB, MythVideo
402  mythdb = MythDB()
403  except MythError, e:
404  print u'\n! Warning - %s' % e.args[0]
405  filename = os.path.expanduser("~")+'/.mythtv/config.xml'
406  if not os.path.isfile(filename):
407  logger.critical(u'A correctly configured (%s) file must exist\n' % filename)
408  else:
409  logger.critical(u'Check that (%s) is correctly configured\n' % filename)
410  sys.exit(1)
411  except Exception, e:
412  logger.critical(u"Creating an instance caused an error for one of: MythDB or "\
413  u"MythVideo, error(%s)\n" % e)
414  sys.exit(1)
415  localhostname = mythdb.gethostname()
416  try:
417  mythbeconn = MythBE(backend=localhostname, db=mythdb)
418  except MythError, e:
419  logger.critical(u'MiroBridge must be run on a MythTV backend, error(%s)' % e.args[0])
420  sys.exit(1)
421 except Exception, e:
422  logger.critical(u"MythTV python bindings could not be imported, error(%s)" % e)
423  sys.exit(1)
424 
425 # Find out if the Miro python bindings can be accessed and instances can be created
426 try:
427  # Initialize locale as required for Miro v3.x
428  try:
429  # Setup gconf_name early on so we can load config values
430  from miro.plat import utils
431  utils.initialize_locale()
432  except:
433  pass
434 
435  # Set up gettext before everything else
436  from miro import config # New for Miro4 (Changed import location)
437  from miro import eventloop # New for Miro4
438  from miro import gtcache
439  config.load() # New for Miro4
440  gtcache.init()
441 
442  # This fixes some/all problems with Python 2.6 support but should be
443  # re-addressed when we have a system with Python 2.6 to test with.
444  # (bug 11262)
445  if sys.version_info[0] == 2 and sys.version_info[1] > 5:
446  import miro.feedparser
447  import miro.storedatabase
448  sys.modules['feedparser'] = miro.feedparser
449  sys.modules['storedatabase'] = miro.storedatabase
450 
451  from miro import prefs
452  from miro import startup
453  from miro import app
454  from miro.frontends.cli.events import EventHandler
455 
456  # Required for Miro 4 as the configuration calls changed location
457  # and additional Miro 4 specific imports are required
458  try:
459  dummy = app.config.get(prefs.APP_VERSION)
460  # A test to see if this is Miro v4 before the version can be read.
461  # If there is no exception this is Miro v4
462  eventloop.setup_config_watcher()
463  from miro import signals
464  from miro import messages
465  from miro import eventloop
466  from miro import feed
467  from miro import workerprocess
468  from miro.frontends.cli.application import InfoUpdaterCallbackList
469  from miro.frontends.cli.application import InfoUpdater
470  from miro.plat.renderers.gstreamerrenderer import movie_data_program_info
471  miroConfiguration = app.config.get
472  from miro import controller
473  app.controller = controller.Controller()
474  except:
475  miroConfiguration = config.get
476  pass
477 
478 except Exception, e:
479  logger.critical(u"Importing Miro functions has an issue. Miro must be installed "\
480  u"and functional, error(%s)", e)
481  sys.exit(1)
482 
483 logger.info(u"Miro Bridge version %s with Miro version %s" % \
484  (__version__, miroConfiguration(prefs.APP_VERSION)))
485 if miroConfiguration(prefs.APP_VERSION) < u"2.0.3":
486  logger.critical((u"Your version of Miro (v%s) is not recent enough. Miro v2.0.3 is "\
487  u"the minimum and it is preferred that you upgrade to Miro v2.5.2 or "\
488  u"later.") % miroConfiguration(prefs.APP_VERSION))
489  sys.exit(1)
490 
491 try:
492  if miroConfiguration(prefs.APP_VERSION) < u"2.5.2":
493  logger.info("Using mirobridge_interpreter_2_0_3")
494  from mirobridge.mirobridge_interpreter_2_0_3 import MiroInterpreter
495  elif miroConfiguration(prefs.APP_VERSION) < u"3.0":
496  logger.info("Using mirobridge_interpreter_2_5_2")
497  from mirobridge.mirobridge_interpreter_2_5_2 import MiroInterpreter
498  elif miroConfiguration(prefs.APP_VERSION) < u"3.5":
499  logger.info("Using mirobridge_interpreter_3_0_0")
500  from mirobridge.mirobridge_interpreter_3_0_0 import MiroInterpreter
501  elif miroConfiguration(prefs.APP_VERSION) < u"4.0":
502  logger.info("Using mirobridge_interpreter_3_5_0")
503  from mirobridge.mirobridge_interpreter_3_5_0 import MiroInterpreter
504  else:
505  logger.info("Using mirobridge_interpreter_4_0_2")
506  from mirobridge.mirobridge_interpreter_4_0_2 import MiroInterpreter
507  from mirobridge.metadata import MetaData
508 except Exception, e:
509  logger.critical(u"Importing mirobridge functions has failed. At least a 'mirobridge_interpreter' "\
510  u"file that matches your Miro version must be in the subdirectory 'mirobridge'.\n'"\
511  u"e.g. mirobridge_interpreter_2_0_3.py', 'mirobridge_interpreter_2_5_2.py' ... etc, "\
512  u"error(%s)" % e)
513  sys.exit(1)
514 
515 def _can_int(x):
516  """Takes a string, checks if it is numeric.
517  >>> _can_int("2")
518  True
519  >>> _can_int("A test")
520  False
521  """
522  if x == None:
523  return False
524  try:
525  int(x)
526  except ValueError:
527  return False
528  else:
529  return True
530 # end _can_int
531 
532 def displayMessage(message):
533  """Displays messages through stdout. Usually used with option (-V) verbose mode.
534  returns nothing
535  """
536  global verbose
537  if verbose:
538  logger.info(message)
539 # end _displayMessage
540 
541 def getExtention(filename):
542  """Get the graphic file extension from a filename
543  return the file extension from the filename
544  """
545  (dirName, fileName) = os.path.split(filename)
546  (fileBaseName, fileExtension)=os.path.splitext(fileName)
547  return fileExtension[1:]
548 # end getExtention
549 
550 
551 def sanitiseFileName(name):
552  '''Take a file name and change it so that invalid or problematic characters are substituted with a "_"
553  return a sanitised valid file name
554  '''
555  global filename_char_filter
556  if name == None or name == u'':
557  return u'_'
558  for char in filename_char_filter:
559  name = name.replace(char, u'_')
560  if name[0] == u'.':
561  name = u'_'+name[1:]
562  return name
563 # end sanitiseFileName()
564 
565 
566 def useImageMagick(cmd):
567  """ Process graphics files using ImageMagick's utility 'mogrify'.
568  >>> useImageMagick('convert screenshot.jpg -resize 50% screenshot.png')
569  >>> 0
570  >>> -1
571  """
572  return subprocess.call(u'%s > /dev/null' % cmd, shell=True)
573 # end useImageMagick()
574 
575 class singleinstance(object):
576  '''
577  singleinstance - based on Windows version by Dragan Jovelic this is a Linux
578  version that accomplishes the same task: make sure that
579  only a single instance of an application is running.
580  '''
581 
582  def __init__(self, pidPath):
583  '''
584  pidPath - full path/filename where pid for running application is to be
585  stored. Often this is ./var/<pgmname>.pid
586  '''
587  from os import kill
588  self.pidPath=pidPath
589  #
590  # See if pidFile exists
591  #
592  if os.path.exists(pidPath):
593  #
594  # Make sure it is not a "stale" pidFile
595  #
596  try:
597  pid=int(open(pidPath, 'r').read().strip())
598  #
599  # Check list of running pids, if not running it is stale so
600  # overwrite
601  #
602  try:
603  kill(pid, 0)
604  pidRunning = 1
605  except OSError:
606  pidRunning = 0
607  if pidRunning:
608  self.lasterror=True
609  else:
610  self.lasterror=False
611  except:
612  self.lasterror=False
613  else:
614  self.lasterror=False
615 
616  if not self.lasterror:
617  #
618  # Write my pid into pidFile to keep multiple copies of program from
619  # running.
620  #
621  fp=open(pidPath, 'w')
622  fp.write(str(os.getpid()))
623  fp.close()
624 
625  def alreadyrunning(self):
626  return self.lasterror
627 
628  def __del__(self):
629  if not self.lasterror:
630  import os
631  os.unlink(self.pidPath)
632  # end singleinstance()
633 
634 def isMiroRunning():
635  '''Check if Miro is running. Only one can be running at the same time.
636  return True if Miro us already running
637  return False if Miro is NOT running
638  '''
639  try:
640  p = subprocess.Popen(u'ps aux | grep "miro.real"', shell=True, bufsize=4096,
641  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
642  close_fds=True)
643  except:
644  return False
645 
646  while True:
647  data = p.stdout.readline()
648  if not data:
649  return False
650  try:
651  data = unicode(data, 'utf8')
652  except (UnicodeEncodeError, TypeError):
653  pass
654  if data.find(u'grep') != -1:
655  continue
656 
657  if data.find(u'miro.real') != -1:
658  logger.critical(u"Miro is already running and therefore Miro Bridge should not be run:")
659  logger.critical(u"(%s)" % data)
660  break
661 
662  return True
663  #end isMiroRunning()
664 
665 
666 # Two routines used for movie title search and matching
667 def is_punct_char(char):
668  '''check if char is punctuation char
669  return True if char is punctuation
670  return False if char is not punctuation
671  '''
672  return char in string.punctuation
673 
674 def is_not_punct_char(char):
675  '''check if char is not punctuation char
676  return True if char is not punctuation
677  return False if char is punctuation
678  '''
679  return not is_punct_char(char)
680 
681 
682 def delOldRecorded(chanid, starttime, endtime, title, \
683  subtitle, description):
684  '''
685  This routine is not supported in the native python bindings as MiroBridge
686  uses the oldrecorded table outside of its original intent.
687  return nothing
688  '''
689  sql_cmd = u"""DELETE FROM `mythconverg`.`oldrecorded`
690  WHERE `oldrecorded`.`chanid` = '%s'
691  AND `oldrecorded`.`starttime` = '%s'
692  AND `oldrecorded`.`endtime` = '%s';"""
693  sql_del_a_record(sql_cmd % (chanid, set_del_datatime(starttime),
694  set_del_datatime(endtime)))
695 # end delOldRecorded()
696 
697 
698 def delRecorded(chanid, starttime):
699  '''Just delete a recorded record. Never abort as sometimes a record
700  may not exist.
701  return nothing
702  '''
703  sql_cmd = u"""DELETE FROM `mythconverg`.`recorded`
704  WHERE `recorded`.`chanid` = %s
705  AND `recorded`.`starttime` = '%s';"""
706  sql_del_a_record(sql_cmd % (chanid, set_del_datatime(starttime)))
707 # end delRecorded()
708 
709 
710 def set_del_datatime(rec_time):
711  ''' Set the SQL datetime so that the delRecorded and delOldrecorded
712  methods use UTC datetime values.
713  return rec_time datetime string
714  '''
715  #
716  return rec_time.astimezone(
717  rec_time.UTCTZ()).strftime("%Y-%m-%d %H:%M:%S")
718 # end set_del_datetime()
719 
720 def sql_del_a_record(sql_cmd):
721  ## Get a MythTV data base cursor
722  cursor = mythdb.cursor()
723  cursor.execute(sql_cmd)
724  cursor.close()
725  #
726  return
727 
728 
729 def hashFile(filename):
730  '''Create metadata hash values for mythvideo files
731  return a hash value
732  return u'' if the was an error with the video file or the video file length was zero bytes
733  '''
734  # Use the MythVideo hashing protocol when the video is in a storage groups
735  if filename[0] != u'/':
736  hash_value = mythbeconn.getHash(filename, u'Videos')
737  if hash_value == u'NULL':
738  return u''
739  else:
740  return hash_value
741 
742  # Use a local hashing routine when video is not in a Videos storage group
743  # For original code: http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#Python
744  try:
745  longlongformat = 'q' # long long
746  bytesize = struct.calcsize(longlongformat)
747  f = open(filename, "rb")
748  filesize = os.path.getsize(filename)
749  hash = filesize
750  if filesize < 65536 * 2: # Video file is too small
751  return u''
752  for x in range(65536/bytesize):
753  buffer = f.read(bytesize)
754  (l_value,)= struct.unpack(longlongformat, buffer)
755  hash += l_value
756  hash = hash & 0xFFFFFFFFFFFFFFFF #to remain as 64bit number
757  f.seek(max(0,filesize-65536),0)
758  for x in range(65536/bytesize):
759  buffer = f.read(bytesize)
760  (l_value,)= struct.unpack(longlongformat, buffer)
761  hash += l_value
762  hash = hash & 0xFFFFFFFFFFFFFFFF
763  f.close()
764  returnedhash = "%016x" % hash
765  return returnedhash
766 
767  except(IOError): # Accessing to this video file caused and error
768  return u''
769 # end hashFile()
770 
771 
772 def rtnAbsolutePath(relpath, filetype=u'mythvideo'):
773  '''Check if there is a Storage Group for the file type (mythvideo, coverfile, banner, fanart,
774  screenshot) and form an appropriate absolute path and file name.
775  return an absolute path and file name
776  return the relpath sting if the file does not actually exist in the absolute path location
777  '''
778  if relpath == None or relpath == u'':
779  return relpath
780 
781  # There is a chance that this is already an absolute path
782  if relpath[0] == u'/':
783  return relpath
784 
785  if not storagegroups.has_key(filetype):
786  return relpath # The Videos storage group path does not exist at all the metadata entry is useless
787 
788  for directory in storagegroups[filetype]:
789  abpath = u"%s/%s" % (directory, relpath)
790  if os.path.isfile(abpath): # The file must actually exist locally
791  return abpath
792  else:
793  return relpath # The relative path does not exist at all the metadata entry is useless
794 # end rtnAbsolutePath
795 
796 
797 def getStorageGroups():
798  '''Populate the storage group dictionary with the host's storage groups.
799  return False if there is an error
800  '''
801  records = mythdb.getStorageGroup(hostname=localhostname)
802  if records:
803  for record in records:
804  if record.groupname in storagegroupnames.keys():
805  try:
806  dirname = unicode(record.dirname, 'utf8')
807  except (UnicodeDecodeError):
808  logger.error((u"The local Storage group (%s) directory contained\ncharacters "\
809  u"that caused a UnicodeDecodeError. This storage group has been "\
810  u"rejected.") % (record.groupname))
811  continue # Skip any line that has non-utf8 characters in it
812  except (UnicodeEncodeError, TypeError):
813  dirname = record.dirname # assume already unicode and pass unchanged
814 
815  # Add a slash if missing to any storage group dirname
816  if dirname[-1:] == u'/':
817  storagegroups[storagegroupnames[record.groupname]] = dirname
818  else:
819  storagegroups[storagegroupnames[record.groupname]] = dirname+u'/'
820  continue
821 
822  if len(storagegroups):
823  # Verify that each storage group is an existing local directory
824  storagegroup_ok = True
825  for key in storagegroups.keys():
826  if not os.path.isdir(storagegroups[key]):
827  logger.critical(u"The Storage group (%s) directory (%s) does not exist" % \
828  (key, storagegroups[key]))
829  storagegroup_ok = False
830  if not storagegroup_ok:
831  sys.exit(1)
832 # end getStorageGroups
833 
835  """
836  Get all video and graphics directories found in the MythTV DB and add them to the dictionary.
837  Ignore any MythTV Frontend setting when there is already a storage group configured.
838  """
839  # Stop processing if this local host has any storage groups
840  global localhostname, vid_graphics_dirs, dir_dict, storagegroups, local_only, verbose
841 
842  # When there is NO SG for Videos then ALL graphics paths MUST be local paths set in the FE and accessable
843  # from the backend
844  if storagegroups.has_key(u'mythvideo'):
845  local_only = False
846  # Pick up storage groups first
847  for key in storagegroups.keys():
848  vid_graphics_dirs[key] = storagegroups[key]
849  for key in dir_dict.keys():
850  if key == u'default' or key == u'mythvideo':
851  continue
852  if not storagegroups.has_key(key):
853  # Set fall back graphics directory to Videos
854  vid_graphics_dirs[key] = storagegroups[u'mythvideo']
855  # Set fall back SG graphics directory to Videos
856  storagegroups[key] = storagegroups[u'mythvideo']
857  else:
858  local_only = True
859  if storagegroups.has_key(u'default'):
860  vid_graphics_dirs[u'default'] = storagegroups[u'default']
861 
862  if local_only:
863  logger.warning(u'There is no "Videos" Storage Group set so ONLY MythTV Frontend local '\
864  u'paths for videos and graphics that are accessable from this MythTV '\
865  u'Backend can be used.')
866 
867  for key in dir_dict.keys():
868  if vid_graphics_dirs[key]:
869  continue
870  graphics_dir = mythdb.settings[localhostname][dir_dict[key]]
871  # Only use path from MythTV if one was found
872  if key == u'mythvideo':
873  if graphics_dir:
874  tmp_directories = graphics_dir.split(u':')
875  if len(tmp_directories):
876  for i in range(len(tmp_directories)):
877  tmp_directories[i] = tmp_directories[i].strip()
878  if tmp_directories[i] != u'':
879  if os.path.exists(tmp_directories[i]):
880  if tmp_directories[i][-1] != u'/':
881  tmp_directories[i]+=u'/'
882  vid_graphics_dirs[key] = tmp_directories[i]
883  break
884  else:
885  logger.error(u"MythVideo video directory (%s) does not exist(%s)" % \
886  (key, tmp_directories[i]))
887  else:
888  logger.error(u"MythVideo video directory (%s) is not set" % (key, ))
889 
890  if key != u'mythvideo':
891  if graphics_dir and os.path.exists(graphics_dir):
892  if graphics_dir[-1] != u'/':
893  graphics_dir+=u'/'
894  vid_graphics_dirs[key] = graphics_dir
895  else: # There is the chance that MythTv DB does not have a dir
896  logger.error(u"(%s) directory is not set or does not exist(%s)" % (key, dir_dict[key]))
897 
898  # Make sure there is a directory set for Videos and other graphics directories on this host
899  dir_for_all = True
900  for key in vid_graphics_dirs.keys():
901  if not vid_graphics_dirs[key]:
902  logger.critical(u"There must be a directory for Videos and each graphics type "\
903  u"the (%s) directory is missing." % (key))
904  dir_for_all = False
905  if not dir_for_all:
906  sys.exit(1)
907 
908  # Make sure that there is read/write access to all the directories Miro Bridge uses
909  access_issue = False
910  for key in vid_graphics_dirs.keys():
911  if not os.access(vid_graphics_dirs[key], os.F_OK | os.R_OK | os.W_OK):
912  logger.critical(u"\nEvery Video and graphics directory must be read/writable for "\
913  u"Miro Bridge to function. There is a permissions issue with (%s)." % \
914  (vid_graphics_dirs[key], ))
915  access_issue = True
916  if access_issue:
917  sys.exit(1)
918 
919  # Print out the video and image directories that will be used for processing
920  if verbose:
921  dir_types={'posterdir' : "Cover art ", 'bannerdir': 'Banners ',
922  'fanartdir' : 'Fan art ', 'episodeimagedir': 'Screenshots',
923  'mythvideo': 'Video ', 'default': 'Default '}
924  sys.stdout.write(u"""
925 ==========================================================================================
926 Listed below are the types and base directories that will be use for processing.
927 The list reflects your current configuration for the '%s' back end
928 and whether a directory is a 'SG' (storage group) or not.
929 Note: All directories are from settings in the MythDB specific to hostname (%s).
930 ------------------------------------------------------------------------------------------
931 """ % (localhostname, localhostname))
932  for key in vid_graphics_dirs.keys():
933  sg_flag = 'NO '
934  if storagegroups.has_key(key):
935  if vid_graphics_dirs[key] == storagegroups[key]:
936  sg_flag = 'YES'
937  sys.stdout.write(u"Type: %s - SG-%s - Directory: (%s)\n" % \
938  (dir_types[key], sg_flag, vid_graphics_dirs[key]))
939  sys.stdout.write(\
940 u"""------------------------------------------------------------------------------------------
941 If a directory you set from a separate Front end is not displayed it means
942 that the directory is not accessible from this backend OR
943 you must add the missing directories using the Front end on this Back end.
944 Front end settings are host machine specific.
945 ==========================================================================================
946 
947 """)
948  # end getMythtvDirectories()
949 
951  """ Change variables through a user supplied configuration file
952  abort the script if there are issues with the configuration file values
953  """
954  global simulation, verbose, channel_icon_override, channel_watch_only, channel_mythvideo_only
955  global vid_graphics_dirs, tv_channels, movie_trailers, filename_char_filter
956 
957  useroptions=u"~/.mythtv/mirobridge.conf"
958 
959  if useroptions[0]==u'~':
960  useroptions=os.path.expanduser(u"~")+useroptions[1:]
961  if os.path.isfile(useroptions) == False:
962  logger.info(
963  u"There was no mirobridge configuration file found (%s)" % useroptions)
964  return
965 
966  cfg = ConfigParser.SafeConfigParser()
967  cfg.read(useroptions)
968  for section in cfg.sections():
969  if section[:5] == u'File ':
970  continue
971  if section == u'variables':
972  for option in cfg.options(section):
973  if option == u'filename_char_filter':
974  for char in cfg.get(section, option):
975  filename_char_filter+=char
976  continue
977  if section == u'icon_override':
978  # Add the Channel names to the array of Channels that are to use their item's icon
979  for option in cfg.options(section):
980  channel_icon_override.append(option)
981  continue
982  if section == u'watch_only':
983  # Add the Channel names to the array of Channels that will only not be moved to MythVideo
984  for option in cfg.options(section):
985  if option == u'all miro channels':
986  channel_watch_only = [u'all']
987  break
988  else:
989  channel_watch_only.append(filter(is_not_punct_char, option.lower()))
990  continue
991  if section == u'mythvideo_only':
992  # Add the Channel names to the array of Channels that will be moved to MythVideo only
993  for option in cfg.options(section):
994  if option == u'all miro channels':
995  channel_mythvideo_only[u'all'] = cfg.get(section, option)
996  break
997  else:
998  channel_mythvideo_only[filter(is_not_punct_char, option.lower())] = \
999  cfg.get(section, option)
1000  for key in channel_mythvideo_only.keys():
1001  if not channel_mythvideo_only[key].startswith(vid_graphics_dirs[u'mythvideo']):
1002  logger.critical((u"All Mythvideo only configuration (%s) directories (%s) must "\
1003  u"be a subdirectory of the MythVideo base directory (%s).") % \
1004  (key, channel_mythvideo_only[key],
1005  vid_graphics_dirs[u'mythvideo']))
1006  sys.exit(1)
1007  if channel_mythvideo_only[key][-1] != u'/':
1008  channel_mythvideo_only[key]+=u'/'
1009  continue
1010  if section == u'watch_then_copy':
1011  # Add the Channel names to the array of Channels once watched will be copied to MythVideo
1012  for option in cfg.options(section):
1013  if option == u'all miro channels':
1014  channel_new_watch_copy[u'all'] = cfg.get(section, option)
1015  break
1016  else:
1017  channel_new_watch_copy[filter(is_not_punct_char, option.lower())] =\
1018  cfg.get(section, option)
1019  for key in channel_new_watch_copy.keys():
1020  if not channel_new_watch_copy[key].startswith(vid_graphics_dirs[u'mythvideo']):
1021  logger.critical((u"All 'new->watch->copy' channel (%s) directory (%s) must "\
1022  u"be a subdirectory of the MythVideo base directory (%s).") % \
1023  (key, channel_new_watch_copy[key],
1024  vid_graphics_dirs[u'mythvideo']))
1025  sys.exit(1)
1026  if channel_new_watch_copy[key][-1] != u'/':
1027  channel_new_watch_copy[key]+=u'/'
1028  continue
1029 # end setUserconfig
1030 
1031 def massageDescription(description, extras=False):
1032  '''Massage the Miro description removing all HTML.
1033  return the massaged description
1034  '''
1035 
1036  def unescape(text):
1037  """Removes HTML or XML character references
1038  and entities from a text string.
1039  @param text The HTML (or XML) source text.
1040  @return The plain text, as a Unicode string, if necessary.
1041  from Fredrik Lundh
1042  2008-01-03: input only unicode characters string.
1043  http://effbot.org/zone/re-sub.htm#unescape-html
1044  """
1045  def fixup(m):
1046  text = m.group(0)
1047  if text[:2] == u"&#":
1048  # character reference
1049  try:
1050  if text[:3] == u"&#x":
1051  return unichr(int(text[3:-1], 16))
1052  else:
1053  return unichr(int(text[2:-1]))
1054  except ValueError:
1055  logger.warn(u"Remove HTML or XML character references: Value Error")
1056  pass
1057  else:
1058  # named entity
1059  # reescape the reserved characters.
1060  try:
1061  if text[1:-1] == u"amp":
1062  text = u"&amp;amp;"
1063  elif text[1:-1] == u"gt":
1064  text = u"&amp;gt;"
1065  elif text[1:-1] == u"lt":
1066  text = u"&amp;lt;"
1067  else:
1068  logger.info(u"%s" % text[1:-1])
1069  text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
1070  except KeyError:
1071  logger.warn(u"Remove HTML or XML character references: keyerror")
1072  pass
1073  return text # leave as is
1074  return re.sub(u"&#?\w+;", fixup, text)
1075 
1076  details = {}
1077  if not description: # Is there anything to massage
1078  if extras:
1079  details[u'plot'] = description
1080  return details
1081  else:
1082  return description
1083 
1084  director_text = u'Director: '
1085  director_re = re.compile(director_text, re.UNICODE)
1086  ratings_text = u'Rating: '
1087  ratings_re = re.compile(ratings_text, re.UNICODE)
1088 
1089  removeText = replaceWith("")
1090  scriptOpen,scriptClose = makeHTMLTags(u"script")
1091  scriptBody = scriptOpen + SkipTo(scriptClose) + scriptClose
1092  scriptBody.setParseAction(removeText)
1093 
1094  anyTag,anyClose = makeHTMLTags(Word(alphas,alphanums+u":_"))
1095  anyTag.setParseAction(removeText)
1096  anyClose.setParseAction(removeText)
1097  htmlComment.setParseAction(removeText)
1098 
1099  commonHTMLEntity.setParseAction(replaceHTMLEntity)
1100 
1101  # first pass, strip out tags and translate entities
1102  firstPass = (htmlComment | scriptBody | commonHTMLEntity |
1103  anyTag | anyClose ).transformString(description)
1104 
1105  # first pass leaves many blank lines, collapse these down
1106  repeatedNewlines = LineEnd() + OneOrMore(LineEnd())
1107  repeatedNewlines.setParseAction(replaceWith(u"\n\n"))
1108  secondPass = repeatedNewlines.transformString(firstPass)
1109  text = secondPass.replace(u"Link to Catalog\n ",u'')
1110  text = unescape(text)
1111 
1112  if extras:
1113  text_lines = text.split(u'\n')
1114  for index in range(len(text_lines)):
1115  text_lines[index] = text_lines[index].rstrip()
1116  index+=1
1117  else:
1118  text_lines = [text.replace(u'\n', u' ')]
1119 
1120  if extras:
1121  description = u''
1122  for text in text_lines:
1123  if len(director_re.findall(text)):
1124  details[u'director'] = text.replace(director_text, u'')
1125  continue
1126  # probe the nature [...]Rating: 3.0/5 (1 vote cast)
1127  if len(ratings_re.findall(text)):
1128  data = text[text.index(ratings_text):].replace(ratings_text, u'')
1129  try:
1130  number = data[:data.index(u'/')]
1131  # HD trailers ratings are our of 5 not 10 like MythTV so must be multiplied by two
1132  try:
1133  details[u'userrating'] = float(number) * 2
1134  except ValueError:
1135  details[u'userrating'] = 0.0
1136  except:
1137  details[u'userrating'] = 0.0
1138  text = text[:text.index(ratings_text)]
1139  if text.rstrip():
1140  description+=text+u' '
1141  else:
1142  description = text_lines[0].replace(u"[...]Rating:", u"[...] Rating:")
1143 
1144  if extras:
1145  details[u'plot'] = description.rstrip()
1146  return details
1147  else:
1148  return description
1149  # end massageDescription()
1150 
1151 
1153  """Retrieves the Miro oldrecorded records for localhostname. Match them against the Miro recorded
1154  records and identify any orphaned oldrecorded records. Those records mean a MythTV user deleted the
1155  Miro video from the Watched Recordings screen or from MythVideo. Delete the orphaned records from
1156  MythTV plus any left over graphic files.
1157  return array of oldrecorded dictionary records which are orphaned
1158  return None if there are no orphaned oldrecorded records
1159  """
1160  global simulation, verbose, channel_id, localhostname, vid_graphics_dirs, statistics
1161  global channel_icon_override
1162  global graphic_suffix, graphic_path_suffix, graphic_name_suffix
1163 
1164  # Convert any old videmetadata copied videos changing their inetref and category values.
1165  # Prevents accidental deletions.
1166  metadata.convertOldMiroVideos()
1167 
1168  recorded_array = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname))
1169  oldrecorded_array = list(mythdb.searchOldRecorded(chanid=channel_id, ))
1170  videometadata = list(mythdb.searchVideos(category=u'Miro'))
1171 
1172  orphans = []
1173  for record in oldrecorded_array:
1174  for recorded in recorded_array:
1175  if recorded[u'starttime'] == record[u'starttime'] and recorded[u'endtime'] == \
1176  record[u'endtime']:
1177  break
1178  else:
1179  for video in videometadata:
1180  if video[u'title'] == record[u'title'] and video[u'subtitle'] == record[u'subtitle']:
1181  break
1182  else:
1183  orphans.append(record)
1184 
1185  for data in orphans:
1186  if simulation:
1187  logger.info(u"Simulation: Remove orphaned oldrecorded record (%s - %s)" % \
1188  (data[u'title'], data[u'subtitle']))
1189  else:
1190  try:
1191  # Sometimes a channel issues videos with identical publishing (starttime) dates.
1192  # Try to using additiional details to identify the correct oldrecord.
1193  delOldRecorded(channel_id, data['starttime'],
1194  data['endtime'], data['title'],
1195  data['subtitle'], data['description'])
1196  except:
1197  pass
1198 
1199  # Attempt a clean up for orphaned recorded video files and/or graphics
1200  metadata.cleanupVideoAndGraphics(u'%s%s_%s.%s' % \
1201  (vid_graphics_dirs[u'default'], channel_id,
1202  data[u'starttime'].strftime('%Y%m%d%H%M%S'), u'png'))
1203 
1204  # Attempt a clean up for orphaned MythVideo files and/or graphics from the Default directory
1205  metadata.cleanupVideoAndGraphics(u'%s%s - %s.%s' % \
1206  (vid_graphics_dirs[u'default'], data[u'title'],
1207  data[u'subtitle'], u'png'))
1208 
1209  # Attempt a clean up for orphaned MythVideo screenshot
1210  metadata.cleanupVideoAndGraphics(u'%s%s - %s%s.%s' % \
1211  (vid_graphics_dirs[u'episodeimagedir'], data[u'title'],
1212  data[u'subtitle'], graphic_suffix[u'episodeimagedir'], u'png'))
1213 
1214  # Remove any unique cover art graphic files
1215  if data[u'title'].lower() in channel_icon_override:
1216  metadata.cleanupVideoAndGraphics(u'%s%s - %s%s.%s' % \
1217  (vid_graphics_dirs[u'posterdir'], data[u'title'],
1218  data[u'subtitle'], graphic_suffix[u'posterdir'], u'png'))
1219 
1220  displayMessage(u"Removed orphaned Miro video and graphics files (%s - %s)" % \
1221  (data[u'title'], data[u'subtitle']))
1222 
1223  return orphans
1224  # end getOldrecordedOrphans()
1225 
1226 
1227 def getStartEndTimes(duration, downloadedTime):
1228  '''Calculate a videos start and end times and isotime for the recorded file name.
1229  return an array of initialised values if either duration or downloadedTime is invalid
1230  return an array of the video's start, end times and isotime
1231  '''
1232  starttime = datetime.now()
1233  end = starttime
1234  start_end = [starttime.strftime('%Y-%m-%d %H:%M:%S'),
1235  starttime.strftime('%Y-%m-%d %H:%M:%S'),
1236  starttime.strftime('%Y%m%d%H%M%S')]
1237 
1238  if downloadedTime != None:
1239  try:
1240  dummy = downloadedTime.strftime('%Y-%m-%d')
1241  except ValueError:
1242  downloadedTime = datetime.now()
1243  end = downloadedTime+timedelta(seconds=duration)
1244  start_end[0] = downloadedTime.strftime('%Y-%m-%d %H:%M:%S')
1245  start_end[1] = end.strftime('%Y-%m-%d %H:%M:%S')
1246  start_end[2] = downloadedTime.strftime('%Y%m%d%H%M%S')
1247 
1248  # Check if there is a Miro video with an identical start time.
1249  # If there is incremement that start and end times by one until unique.
1250  while True:
1251  if not len(list(mythdb.searchOldRecorded(chanid=channel_id, starttime=start_end[0]))):
1252  break
1253  starttime = starttime + timedelta(0,1)
1254  end = end + timedelta(0,1)
1255  start_end[0] = starttime.strftime('%Y-%m-%d %H:%M:%S')
1256  start_end[1] = end.strftime('%Y-%m-%d %H:%M:%S')
1257  start_end[2] = starttime.strftime('%Y%m%d%H%M%S')
1258  continue
1259 
1260  return start_end
1261  # end getStartEndTimes()
1262 
1263 def setSymbolic(filename, storagegroupkey, symbolic_name, allow_symlink=False):
1264  '''Convert the file into a symbolic name according to it's storage group (there may be
1265  no storage group for the key). Check if a symbolic link exists and replace the link with a copy of
1266  the file. except for video files. Abort if the file does not exist.
1267  return the symbolic link to the file
1268  '''
1269  global simulation, verbose, storagegroups, vid_graphics_dirs
1270  global graphic_suffix, graphic_path_suffix, graphic_name_suffix
1271  global local_only
1272 
1273  if not os.path.isfile(filename):
1274  logger.error(u"The file (%s) must exist to create a symbolic link" % filename)
1275  return None
1276 
1277  ext = getExtention(filename)
1278  if ext.lower() == u'm4v':
1279  ext = u'mpg'
1280 
1281  convert = False
1282  if ext.lower() in [u'gif', u'jpeg', u'JPG', ]: # convert graphics to standard jpg and png types
1283  ext = u'jpg'
1284  convert = True
1285 
1286  if storagegroupkey in storagegroups.keys() and storagegroupkey == u'default':
1287  sym_filepath = graphic_path_suffix % (storagegroups[storagegroupkey], symbolic_name,
1288  graphic_suffix[storagegroupkey], ext)
1289  sym_filename = graphic_name_suffix % (symbolic_name, graphic_suffix[storagegroupkey], ext)
1290  elif storagegroupkey in storagegroups.keys() and not local_only:
1291  sym_filepath = graphic_path_suffix % (storagegroups[storagegroupkey], symbolic_name,
1292  graphic_suffix[storagegroupkey], ext)
1293  sym_filename = graphic_name_suffix % (symbolic_name, graphic_suffix[storagegroupkey], ext)
1294  else:
1295  sym_filepath = graphic_path_suffix % (vid_graphics_dirs[storagegroupkey], symbolic_name,
1296  graphic_suffix[storagegroupkey], ext)
1297  sym_filename = sym_filepath
1298 
1299  if allow_symlink:
1300  if os.path.isfile(os.path.realpath(sym_filepath)):
1301  return sym_filename
1302  else:
1303  if os.path.isfile(os.path.realpath(sym_filepath)) and not os.path.islink(sym_filepath):
1304  return sym_filename # file already exists
1305  try: # Try to remove a broken symbolic link
1306  os.remove(sym_filepath)
1307  except OSError:
1308  pass
1309 
1310  if simulation:
1311  if allow_symlink:
1312  logger.info(u"Simulation: Used file (%s) to create symlink as (%s)" % \
1313  (filename, sym_filepath))
1314  else:
1315  logger.info(u"Simulation: Used file (%s) copy as (%s)" % (filename, sym_filepath))
1316  return sym_filename
1317 
1318  try:
1319  if allow_symlink:
1320  os.symlink(filename, sym_filepath)
1321  displayMessage(u"Used file (%s) to created symlink (%s)" % (filename, sym_filepath))
1322  else:
1323  try: # Every file but video files copied to support Storage groups
1324  if convert:
1325  useImageMagick(u'convert "%s" "%s"' % (filename, sym_filepath))
1326  displayMessage(u"Convert and copy Miro file (%s) to file (%s)" % \
1327  (filename, sym_filepath))
1328  else:
1329  shutil.copy2(filename, sym_filepath)
1330  displayMessage(u"Copied Miro file (%s) to file (%s)" % (filename, sym_filepath))
1331  except OSError, e:
1332  logger.critical((u"Trying to copy the Miro file (%s) to the file (%s).\nError(%s)"\
1333  u"\nThis maybe a permissions error (mirobridge.py does not have "\
1334  u"permission to write to the directory).") % \
1335  (filename ,sym_filepath, e))
1336  sys.exit(1)
1337  except OSError, e:
1338  logger.critical((u"Trying to create the Miro file (%s) symlink (%s).\nError(%s)\nThis "\
1339  u"maybe a permissions error (mirobridge.py does not have permission to "\
1340  u"write to the directory).") % (sym_filepath, e))
1341  sys.exit(1)
1342 
1343  return sym_filename
1344  # end setSymbolic()
1345 
1346 
1347 def createOldRecordedRecord(item, start, end, inetref):
1348  '''Using the details from a Miro item record create a MythTV oldrecorded record
1349  return an array of MythTV of a full initialised oldrecorded record dictionaries
1350  '''
1351  global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id
1352 
1353  tmp_oldrecorded={}
1354 
1355  # Create the oldrecorded dictionary
1356  tmp_oldrecorded[u'chanid'] = channel_id
1357  tmp_oldrecorded[u'starttime'] = start
1358  tmp_oldrecorded[u'endtime'] = end
1359  tmp_oldrecorded[u'title'] = item[u'channelTitle']
1360  tmp_oldrecorded[u'subtitle'] = item[u'title']
1361  tmp_oldrecorded[u'category'] = u'Miro'
1362  tmp_oldrecorded[u'station'] = u'MIRO'
1363  tmp_oldrecorded[u'inetref'] = inetref
1364  tmp_oldrecorded[u'season'] = item[u'season']
1365  tmp_oldrecorded[u'episode'] = item[u'episode']
1366 
1367  try:
1368  tmp_oldrecorded[u'description'] = massageDescription(item[u'description'])
1369  except TypeError:
1370  tmp_oldrecorded[u'description'] = item[u'description']
1371  return tmp_oldrecorded
1372  # end createOldRecordedRecord()
1373 
1374 
1376  '''Using the details from a Miro item record create a MythTV recorded record
1377  return an array of MythTV full initialised recorded and recordedprogram record dictionaries
1378  '''
1379  global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id
1380 
1381  tmp_recorded={}
1382  tmp_recordedprogram={}
1383 
1384  # Get any graphics that already exist
1385  graphics = metadata.getMetadata(sanitiseFileName(item[u'channelTitle']))
1386 
1387  ffmpeg_details = metadata.getVideoDetails(item[u'videoFilename'])
1388  start_end = getStartEndTimes(ffmpeg_details[u'duration'], item[u'downloadedTime'])
1389 
1390  if item[u'releasedate'] == None:
1391  item[u'releasedate'] = item[u'downloadedTime']
1392  try:
1393  dummy = item[u'releasedate'].strftime('%Y-%m-%d')
1394  except ValueError:
1395  item[u'releasedate'] = item[u'downloadedTime']
1396 
1397  # Create the recorded dictionary
1398  tmp_recorded[u'chanid'] = channel_id
1399  tmp_recorded[u'starttime'] = start_end[0]
1400  tmp_recorded[u'endtime'] = start_end[1]
1401  tmp_recorded[u'title'] = item[u'channelTitle']
1402  tmp_recorded[u'subtitle'] = item[u'title']
1403  tmp_recorded[u'season'] = item[u'season']
1404  tmp_recorded[u'episode'] = item[u'episode']
1405  tmp_recorded[u'inetref'] = graphics[u'inetref']
1406  try:
1407  tmp_recorded[u'description'] = massageDescription(item[u'description'])
1408  except TypeError:
1409  print
1410  print u"Channel title(%s) subtitle(%s)" % (item[u'channelTitle'], item[u'title'])
1411  print u"The 'massageDescription()' function could not remove HTML and XML tags from:"
1412  print u"Description (%s)\n\n" % item[u'description']
1413  tmp_recorded[u'description'] = item[u'description']
1414  tmp_recorded[u'category'] = u'Miro'
1415  tmp_recorded[u'hostname'] = localhostname
1416  tmp_recorded[u'lastmodified'] = tmp_recorded[u'endtime']
1417  tmp_recorded[u'filesize'] = item[u'size']
1418  if item[u'releasedate'] != None:
1419  tmp_recorded[u'originalairdate'] = item[u'releasedate'].strftime('%Y-%m-%d')
1420 
1421  basename = setSymbolic(item[u'videoFilename'], u'default', u"%s_%s" % \
1422  (channel_id, start_end[2]), allow_symlink=True)
1423  if basename != None:
1424  tmp_recorded[u'basename'] = basename
1425  else:
1426  logger.critical(u"The file (%s) must exist to create a recorded record" % \
1427  item[u'videoFilename'])
1428  sys.exit(1)
1429 
1430  tmp_recorded[u'progstart'] = start_end[0]
1431  tmp_recorded[u'progend'] = start_end[1]
1432 
1433  # Create the recordedpropgram dictionary
1434  tmp_recordedprogram[u'chanid'] = channel_id
1435  tmp_recordedprogram[u'starttime'] = start_end[0]
1436  tmp_recordedprogram[u'endtime'] = start_end[1]
1437  tmp_recordedprogram[u'title'] = item[u'channelTitle']
1438  tmp_recordedprogram[u'subtitle'] = item[u'title']
1439  try:
1440  tmp_recordedprogram[u'description'] = massageDescription(item[u'description'])
1441  except TypeError:
1442  tmp_recordedprogram[u'description'] = item[u'description']
1443 
1444  tmp_recordedprogram[u'category'] = u"Miro"
1445  tmp_recordedprogram[u'category_type'] = u"series"
1446  if item[u'releasedate'] != None:
1447  tmp_recordedprogram[u'airdate'] = item[u'releasedate'].strftime('%Y')
1448  tmp_recordedprogram[u'originalairdate'] = item[u'releasedate'].strftime('%Y-%m-%d')
1449  tmp_recordedprogram[u'stereo'] = ffmpeg_details[u'stereo']
1450  tmp_recordedprogram[u'hdtv'] = ffmpeg_details[u'hdtv']
1451  tmp_recordedprogram[u'audioprop'] = ffmpeg_details[u'audio']
1452  tmp_recordedprogram[u'videoprop'] = ffmpeg_details[u'video']
1453 
1454  return [tmp_recorded, tmp_recordedprogram,
1455  createOldRecordedRecord(item, start_end[0], start_end[1], graphics[u'inetref'])]
1456  # end createRecordedRecord()
1457 
1458 
1460  '''Using the details from a Miro item create a MythTV videometadata record
1461  return an dictionary of MythTV an initialised videometadata record
1462  '''
1463  global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id
1464  global vid_graphics_dirs, channel_icon_override, flat, image_extensions
1465  global local_only
1466 
1467  ffmpeg_details = metadata.getVideoDetails(item[u'videoFilename'])
1468  start_end = getStartEndTimes(ffmpeg_details[u'duration'], item[u'downloadedTime'])
1469 
1470  sympath = u'Miro'
1471  if not flat:
1472  sympath+=u"/%s" % item[u'channelTitle']
1473 
1474  # Get any graphics that already exist
1475  graphics = metadata.getMetadata(sanitiseFileName(item[u'channelTitle']))
1476 
1477  videometadata = {}
1478 
1479  videometadata[u'title'] = item[u'channelTitle']
1480  videometadata[u'subtitle'] = item[u'title']
1481  videometadata[u'inetref'] = graphics[u'inetref']
1482  videometadata[u'season'] = item[u'season']
1483  videometadata[u'episode'] = item[u'episode']
1484 
1485  try:
1486  details = massageDescription(item[u'description'], extras=True)
1487  except TypeError:
1488  print
1489  print u"MythVideo-Channel title(%s) subtitle(%s)" % \
1490  (item[u'channelTitle'], item[u'title'])
1491  print u"The 'massageDescription()' function could not remove HTML and XML tags from:"
1492  print u"Description (%s)\n\n" % item[u'description']
1493  details = {u'plot': item[u'description']}
1494 
1495  for key in details.keys():
1496  videometadata[key] = details[key]
1497 
1498  if item[u'releasedate'] == None:
1499  item[u'releasedate'] = item[u'downloadedTime']
1500  try:
1501  dummy = item[u'releasedate'].strftime('%Y-%m-%d')
1502  except ValueError:
1503  item[u'releasedate'] = item[u'downloadedTime']
1504 
1505  if item[u'releasedate'] != None:
1506  videometadata[u'year'] = item[u'releasedate'].strftime('%Y')
1507  videometadata[u'releasedate'] = item[u'releasedate'].strftime('%Y-%m-%d')
1508  videometadata[u'length'] = ffmpeg_details[u'duration']/60
1509  if item.has_key(u'copied'):
1510  videometadata[u'category'] = u'Video Cast'
1511  else:
1512  videometadata[u'category'] = u'Miro'
1513 
1514  if not item.has_key(u'copied'):
1515  videofile = setSymbolic(item[u'videoFilename'], u'mythvideo', "%s/%s - %s" % \
1516  (sympath, sanitiseFileName(item[u'channelTitle']),
1517  sanitiseFileName(item[u'title'])), allow_symlink=True)
1518  if videofile != None:
1519  videometadata[u'filename'] = videofile
1520  if not local_only and videometadata[u'filename'][0] != u'/':
1521  videometadata[u'host'] = localhostname.lower()
1522  else:
1523  logger.critical(u"The file (%s) must exist to create a videometadata record" % \
1524  item[u'videoFilename'])
1525  sys.exit(1)
1526  else:
1527  videometadata[u'filename'] = item[u'videoFilename']
1528  if not local_only and videometadata[u'filename'][0] != u'/':
1529  videometadata[u'host'] = localhostname.lower()
1530 
1531  videometadata[u'hash'] = hashFile(videometadata[u'filename'])
1532 
1533  if not item.has_key(u'copied'):
1534  if graphics['coverart']:
1535  videometadata[u'coverfile'] = graphics['coverart']
1536  elif item[u'channel_icon'] and not item[u'channelTitle'].lower() in channel_icon_override:
1537  filename = setSymbolic(item[u'channel_icon'], u'posterdir', u"%s" % \
1538  (sanitiseFileName(item[u'channelTitle'])))
1539  if filename != None:
1540  videometadata[u'coverfile'] = filename
1541  else:
1542  if item[u'item_icon']:
1543  filename = setSymbolic(item[u'item_icon'], u'posterdir', u"%s - %s" % \
1544  (sanitiseFileName(item[u'channelTitle']),
1545  sanitiseFileName(item[u'title'])))
1546  if filename != None:
1547  videometadata[u'coverfile'] = filename
1548  else:
1549  videometadata[u'coverfile'] = item[u'channel_icon']
1550 
1551  if not item.has_key(u'copied'):
1552  if item[u'screenshot']:
1553  filename = setSymbolic(item[u'screenshot'], u'episodeimagedir', u"%s - %s" % \
1554  (sanitiseFileName(item[u'channelTitle']),
1555  sanitiseFileName(item[u'title'])))
1556  if filename != None:
1557  videometadata[u'screenshot'] = filename
1558  else:
1559  if item[u'screenshot']:
1560  videometadata[u'screenshot'] = item[u'screenshot']
1561 
1562  if graphics['banner']:
1563  videometadata[u'banner'] = graphics['banner']
1564  if graphics['fanart']:
1565  videometadata[u'fanart'] = graphics['fanart']
1566 
1567  return [videometadata, createOldRecordedRecord(item, start_end[0], start_end[1],
1568  graphics[u'inetref'])]
1569  # end createVideometadataRecord()
1570 
1571 
1572 def createChannelRecord(icon, channel_id, channel_num):
1573  '''
1574  Create the optional Miro "channel" record as it makes the Watch Recordings screen look better.
1575  return True if the record was created and written successfully
1576  abort if the processing failed
1577  '''
1578  global localhostname, simulation, verbose, vid_graphics_dirs
1579 
1580  if icon != u"":
1581  if not os.path.isfile(icon):
1582  logger.critical((u'The Miro channel icon file (%s) does not exist.\nThe variable '\
1583  u'needs to be a fully qualified file name and path.\ne.g. '\
1584  u'./mirobridge.py -C "/path to the channel icon/miro_channel_icon.'\
1585  u'jpg"') % (icon))
1586  sys.exit(1)
1587 
1588  data={}
1589  data['chanid'] = channel_id
1590  data['channum'] = str(channel_num)
1591  data['freqid'] = str(channel_num)
1592  data['atsc_major_chan'] = int(channel_num)
1593  data['icon'] = u''
1594  if icon != u'':
1595  data['icon'] = icon
1596  data['callsign'] = u'Miro'
1597  data['name'] = u'Miro'
1598  data['last_record'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1599 
1600  if simulation:
1601  logger.info(u"Simulation: Create Miro channel record channel_id(%d) and channel_num(%d)" % \
1602  (channel_id, channel_num))
1603  logger.info(u"Simulation: Channel icon file(%s)" % (icon))
1604  else:
1605  try:
1606  Channel().create(data)
1607  except MythError, e:
1608  logger.critical((u"Failed writing the Miro channel record. Most likely the Channel "\
1609  u"Id and number already exists.\nUse MythTV set up program "\
1610  u"(mythtv-setup) to alter or remove the offending channel.\nYou "\
1611  u"specified Channel ID (%d) and Channel Number (%d), error(%s)") % \
1612  (channel_id, channel_num, e.args[0]))
1613  sys.exit(1)
1614  return True
1615  # end createChannelRecord(icon)
1616 
1617 
1618 def checkVideometadataFails(record, flat):
1619  '''
1620  Verify that the real path exists for both video and graphics for this MythVideo Miro record.
1621  return False if there were no failures
1622  return True if a failure was found
1623  '''
1624  global localhostname, verbose, vid_graphics_dirs, key_trans
1625 
1626  for field in key_trans.keys():
1627  if not record[field]:
1628  continue
1629  if record[field] == u'No Cover':
1630  continue
1631  if not record[u'host'] or record[field][0] == u'/':
1632  if not os.path.isfile(record[field]):
1633  return True
1634  else:
1635  if not os.path.isfile(vid_graphics_dirs[key_trans[field]]+record[field]):
1636  return True
1637  return False
1638  # end checkVideometadataFails()
1639 
1640 
1642  '''If the "Miro" directory does not exist in MythVideo then create it.
1643  abort if there is an issue creating the directory
1644  '''
1645  global localhostname, vid_graphics_dirs, storagegroups, channel_id, flat, simulation, verbose
1646 
1647  # Check that a MIRO video directory exists
1648  # if not then create the dir and add symbolic link to cover/file or icon
1649  miro = u'Miro'
1650  miro_path = vid_graphics_dirs[u'mythvideo']+miro
1651  if not os.path.isdir(miro_path):
1652  try:
1653  if simulation:
1654  logger.info(u"Simulation: Create Miro Mythvideo directory (%s)" % (miro_path,))
1655  else:
1656  try:
1657  os.mkdir(miro_path)
1658  except OSError, e:
1659  logger.critical((u"Create Miro Mythvideo directory (%s).\nError(%s)\n" \
1660  u"This may be due to a permissions error.") % \
1661  (miro_path, e))
1662  sys.exit(1)
1663  except OSError, e:
1664  logger.critical((u"Creation of MythVideo 'Miro' directory (%s) failed.\nError(%s)"\
1665  u"\nThis may be due to a permissions error.") % (miro_path, e))
1666  sys.exit(1)
1667  # end createMiroMythVideoDirectory()
1668 
1670  '''Create the Miro Channel subdirectory in MythVideo
1671  abort if the subdirectory cannot be made
1672  '''
1673  global localhostname, vid_graphics_dirs, storagegroups, channel_id, flat, simulation, verbose
1674 
1675  miro = u'Miro'
1676  path = u"%s%s/%s" % (vid_graphics_dirs[u'mythvideo'], miro,
1677  sanitiseFileName(item[u'channelTitle']))
1678 
1679  if simulation:
1680  logger.info(u"Simulation: Make subdirectory(%s)" % (path))
1681  else:
1682  if not os.path.isdir(path):
1683  try:
1684  os.mkdir(path)
1685  except OSError, e:
1686  logger.critical((u"Creation of MythVideo 'Miro' subdirectory path (%s) failed."\
1687  u"\nError(%s)\nThis may be due to a permissions error.") % \
1688  (path, e))
1689  sys.exit(1)
1690  # end createMiroChannelSubdirectory()
1691 
1692 
1694  '''From the MythTV database recorded records identify all "played" Miro Video files.
1695  return None if there were either no Miro recorded records or none that were in "watched" status
1696  return an array of subtitles of those Miro video files that were "watched"
1697  '''
1698  global localhostname, vid_graphics_dirs, storagegroups, verbose, channel_id, statistics
1699 
1700  filenames=[]
1701  recorded = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname))
1702  for record in recorded:
1703  if record[u'watched'] == 0: # Skip if the video has NOT been watched
1704  continue
1705  try:
1706  filenames.append(os.path.realpath(storagegroups[u'default']+record[u'basename']))
1707  statistics[u'WR_watched']+=1
1708  except OSError, e:
1709  logger.info(u"Miro video file has been removed (%s) outside of mirobridge\nError(%s)" % \
1710  (storagegroups[u'default']+record[u'basename'], e))
1711  continue
1712  displayMessage(u"Miro video (%s) (%s) has been marked as watched in MythTV." % \
1713  (record[u'title'], record[u'subtitle']))
1714  if len(filenames):
1715  return filenames
1716  else:
1717  return None
1718  # end getPlayedMiroVideos()
1719 
1721  '''
1722  Add and delete MythTV (Watch Recordings) Miro recorded records. Add and delete symbolic links
1723  to coverart/Miro icons.
1724  Abort if processing failed
1725  return True if processing was successful
1726  '''
1727  global localhostname, vid_graphics_dirs, storagegroups, channel_id, simulation, imagemagick
1728  global graphic_suffix, graphic_path_suffix, graphic_name_suffix
1729 
1730  if not items: # There may not be any new items but a clean up of existing recorded may be required
1731  items = []
1732  items_copy = list(items)
1733 
1734  # Deal with existing Miro videos already in the MythTV data base
1735  recorded = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname))
1736  for record in recorded:
1737  if storagegroups.has_key(u'default'):
1738  sym_filepath = u"%s%s" % (storagegroups[u'default'], record[u'basename'])
1739  else:
1740  sym_filepath = u"%s%s" % (vid_graphics_dirs[u'default'], record[u'basename'])
1741  # Remove any videos that were marked as viewed within Miro but NOT MythTV
1742  remove = False
1743  for item in items:
1744  if item[u'channelTitle'] == record[u'title'] and item[u'title'] == record[u'subtitle']:
1745  break
1746  else:
1747  remove = True
1748  # Remove any Miro related "watched" recordings (symlink, recorded records)
1749  # Or remove any Miro related with broken symbolic video links
1750  if record[u'watched'] == 1 or not os.path.isfile(os.path.realpath(sym_filepath)):
1751  remove = True
1752  if remove:
1753  displayMessage(u"Removing watched Miro recording (%s) (%s)" % \
1754  (record[u'title'], record[u'subtitle']))
1755  # Remove the database recorded and oldrecorded records
1756  if simulation:
1757  logger.info(u"Simulation: Remove recorded/recordedprogram/oldrecorded records and "\
1758  u"associated Miro Video file for chanid(%s), starttime(%s)" % \
1759  (record['chanid'], record['starttime']))
1760  else:
1761  try:
1762  # Attempting to clean up an recorded record and
1763  # its associated video file (miro symlink)
1764  rtn = delRecorded(record['chanid'], record['starttime'])
1765  except MythError, e:
1766  pass
1767 
1768  # Clean up for recorded video files and/or graphics
1769  metadata.cleanupVideoAndGraphics(u'%s%s_%s.%s' % \
1770  (vid_graphics_dirs[u'default'], record['chanid'],
1771  record[u'starttime'].strftime('%Y%m%d%H%M%S'), u'png'))
1772 
1773  try: # Attempting to clean up an orphaned oldrecorded record which may or may not exist
1774  rtn = delOldRecorded(record['chanid'], record['starttime'],
1775  record['endtime'], record['title'],
1776  record['subtitle'], record['description'])
1777  except Exception, e:
1778  pass
1779 
1780  recorded = list(mythdb.searchRecorded(chanid=channel_id, hostname=localhostname))
1781  for record in recorded: # Skip any item already in MythTV data base
1782  for item in items:
1783  if item[u'channelTitle'] == record[u'title'] and item[u'title'] == record[u'subtitle']:
1784  items_copy.remove(item)
1785  break
1786 
1787  # Add new Miro unwatched videos to MythTV'd data base
1788  for item in items_copy:
1789  # Do not create records for Miro video files when Miro has a corrupt or missing file name
1790  if item[u'videoFilename'] == None:
1791  continue
1792  # Do not create records for Miro video files that do not exist
1793  if not os.path.isfile(os.path.realpath(item[u'videoFilename'])):
1794  continue
1795  if not os.path.isfile(os.path.realpath(item[u'videoFilename'])):
1796  continue # Do not create records for Miro video files that do not exist
1797  records = createRecordedRecords(item)
1798  if records:
1799  if simulation:
1800  logger.info(u"Simulation: Added recorded and recordedprogram records for "\
1801  u"(%s - %s)" % (item[u'channelTitle'], item[u'title'],))
1802  else:
1803  try:
1804  Recorded().create(records[0])
1805  RecordedProgram().create(records[1])
1806  OldRecorded().create(records[2])
1807  except MythError, e:
1808  logger.warning(u"Inserting recorded/recordedprogram/oldrecorded records "\
1809  u"non-critical error (%s) for (%s - %s)" % \
1810  (e.args[0], item[u'channelTitle'], item[u'title']))
1811  else:
1812  logger.critical(u"Creation of recorded/recordedprogram/oldrecorded record data for "\
1813  u"(%s - %s)" % (item[u'channelTitle'], item[u'title'],))
1814  sys.exit(1)
1815 
1816  if item[u'channel_icon']: # Add Cover art link to channel icon
1817  ext = getExtention(item[u'channel_icon'])
1818  coverart_filename = u"%s%s%s.%s" % (vid_graphics_dirs[u'posterdir'],
1819  sanitiseFileName(item[u'channelTitle']),
1820  graphic_suffix[u'posterdir'], ext)
1821  if not os.path.isfile(os.path.realpath(coverart_filename)): # Make sure it does not exist
1822  if simulation:
1823  logger.info(u"Simulation: Remove symbolic link(%s)" % (coverart_filename,))
1824  else:
1825  try: # Clean up broken symbolic link
1826  os.remove(coverart_filename)
1827  except OSError:
1828  pass
1829  if simulation:
1830  logger.info(u"Simulation: Create icon file(%s) cover art file(%s)" % \
1831  (item[u'channel_icon'], coverart_filename))
1832  else:
1833  try:
1834  shutil.copy2(item[u'channel_icon'], coverart_filename)
1835  displayMessage((u"Copied a Miro Channel Icon file (%s) to MythTV as "\
1836  u"file (%s).") % (item[u'channel_icon'], coverart_filename))
1837  except OSError, e:
1838  logger.critical((u"Copying an icon file(%s) to coverart file(%s) failed."\
1839  u"\nError(%s)\nThis may be due to a permissions error.") % \
1840  (item[u'channel_icon'], coverart_filename, e))
1841  sys.exit(1)
1842 
1843  if item[u'screenshot'] and imagemagick: # Add Miro screen shot to 'Default' recordings directory
1844  screenshot_recorded = u"%s%s.png" % (vid_graphics_dirs[u'default'], records[0][u'basename'])
1845  if not os.path.isfile(screenshot_recorded): # Make sure it does not exist
1846  if simulation:
1847  logger.info(u"Simulation: Create screenshot file(%s) as(%s)" % \
1848  (item[u'screenshot'], screenshot_recorded))
1849  else:
1850  try:
1851  demensions = u''
1852  try:
1853  demensions = takeScreenShot(item[u'videoFilename'],
1854  screenshot_recorded,
1855  size_limit=True,
1856  just_demensions=False)
1857  except:
1858  pass
1859  if demensions:
1860  demensions = u"-size %s" % demensions
1861  useImageMagick(u'convert "%s" %s "%s"' % \
1862  (item[u'screenshot'], demensions,
1863  screenshot_recorded))
1864  displayMessage((u"Used a Miro Channel screenshot file (%s) to\ncreate "\
1865  u"using ImageMagick the MythTV Watch Recordings screen "\
1866  u"shot file\n(%s).") % \
1867  (item[u'screenshot'], screenshot_recorded))
1868  except OSError, e:
1869  logger.critical((u"Creating screenshot file(%s) as(%s) failed.\nError"\
1870  u"(%s)\nThis may be due to a permissions error.") % \
1871  (item[u'screenshot'], screenshot_recorded, e))
1872  sys.exit(1)
1873  else:
1874  screenshot_recorded = u"%s%s.png" % \
1875  (vid_graphics_dirs[u'default'], records[0][u'basename'])
1876  try:
1877  takeScreenShot(item[u'videoFilename'], screenshot_recorded, size_limit=True)
1878  except:
1879  pass
1880 
1881  return True
1882  # updateMythRecorded()
1883 
1884 def updateMythVideo(items):
1885  '''Add and delete MythVideo records for played Miro Videos. Add and delete symbolic links
1886  to Miro Videos, to coverart/Miro icons, banners and Miro screenshots and fanart.
1887  NOTE: banner and fanart graphics were provided with the script and are used only if present.
1888  Abort if processing failed
1889  return True if processing was successful
1890  '''
1891  global localhostname, vid_graphics_dirs, storagegroups, channel_id, flat, simulation, verbose
1892  global channel_watch_only, statistics
1893  global graphic_suffix, graphic_path_suffix, graphic_name_suffix
1894  global local_only
1895 
1896  if not items: # There may not be any new items but a clean up of existing records may be required
1897  items = []
1898  # Check that a MIRO video directory exists
1899  # if not then create the dir and add symbolic link to cover/file or icon
1901 
1902  # Remove any Miro Mythvideo records which the video or graphics paths are broken
1903  records = list(mythdb.searchVideos(category=u'Miro'))
1904  statistics[u'Total_Miro_MythVideos'] = len(records)
1905  for record in records: # Count the Miro-MythVideos that Miro is expiring or has saved
1906  if record[u'filename'][0] == u'/':
1907  if os.path.islink(record[u'filename']) and os.path.isfile(record[u'filename']):
1908  statistics[u'Total_Miro_expiring']+=1
1909  elif record[u'host'] and storagegroups.has_key(u'mythvideo'):
1910  if os.path.islink(storagegroups[u'mythvideo']+record[u'filename']) and \
1911  os.path.isfile(storagegroups[u'mythvideo']+record[u'filename']):
1912  statistics[u'Total_Miro_expiring']+=1
1913  for record in records:
1914  if checkVideometadataFails(record, flat):
1915  delete = False
1916  if os.path.islink(record[u'filename']): # Only delete video files if they are symlinks
1917  if not record[u'host'] or record[u'filename'][0] == '/':
1918  if not os.path.isfile(record[u'filename']):
1919  delete = True
1920  else:
1921  if not os.path.isfile(vid_graphics_dirs[key_trans[field]]+record[u'filename']):
1922  delete = True
1923  else:
1924  if not os.path.isfile(record[u'filename']):
1925  delete = True
1926  if delete: # Only delete video files if they are symlinks
1927  if simulation:
1928  logger.info(u"Simulation: DELETE videometadata for intid = %s" % \
1929  (record[u'intid'],))
1930  logger.info(u"Simulation: DELETE oldrecorded for title(%s), subtitle(%s)" % \
1931  (record[u'title'], record[u'subtitle']))
1932  else:
1933  rtn = Video(record[u'intid'], db=mythdb).delete()
1934  try: # An orphaned oldrecorded record may not exist
1935  for oldrecorded in mythdb.searchOldRecorded(title=record[u'title'],
1936  subtitle=record[u'subtitle'] ):
1937  rtn = delOldRecorded(channel_id,
1938  oldrecorded['starttime'],
1939  oldrecorded['endtime'],
1940  oldrecorded['title'],
1941  oldrecorded['subtitle'],
1942  oldrecorded['description'])
1943  except Exception, e:
1944  pass
1945  statistics[u'Total_Miro_MythVideos']-=1
1946  # Remove video file
1947  metadata.deleteFile(record[u'filename'], record[u'host'], u'mythvideo')
1948  if record[u'screenshot']: # Remove any associated Screenshot
1949  metadata.deleteFile(record[u'screenshot'], record[u'host'], u'episodeimagedir')
1950  # Remove any unique cover art graphic files
1951  if record[u'title'].lower() in channel_icon_override:
1952  metadata.deleteFile(record[u'coverfile'], record[u'host'], u'posterdir')
1953 
1954  if not items: # There may not be any new items to add to MythVideo
1955  return True
1956  # Reread Miro Mythvideo videometadata records
1957  # Remove the matching videometadata record from array of items
1958  items_copy = list(items)
1959  records = list(mythdb.searchVideos(category=u'Miro'))
1960  for record in records:
1961  for item in items:
1962  if item[u'channelTitle'] == record[u'title'] and item[u'title'] == record[u'subtitle']:
1963  try:
1964  items_copy.remove(item)
1965  except ValueError:
1966  logger.info((u"Video (%s - %s) was found multiple times in list of (watched "\
1967  u"and/or saved) items from Miro - skipping") % \
1968  (item[u'channelTitle'], item[u'title']))
1969  pass
1970  break
1971 
1972  for item in items: # Remove any items that are for a Channel that does not get MythVideo records
1973  if filter(is_not_punct_char, item[u'channelTitle'].lower()) in channel_watch_only:
1974  try: # Some items may have already been removed, let those passed
1975  items_copy.remove(item)
1976  except ValueError:
1977  pass
1978  # Add Miro videos that remain in the item list
1979  # If not a flat directory check if title directory exists and add icon symbolic link as coverfile
1980  for item in items_copy:
1981  if not flat and not item.has_key(u'copied'):
1983  if not item[u'screenshot']: # If there is no screen shot then create one
1984  screenshot_mythvideo = u"%s%s - %s%s.jpg" % \
1985  (vid_graphics_dirs[u'episodeimagedir'],
1986  sanitiseFileName(item[u'channelTitle']),
1987  sanitiseFileName(item[u'title']),
1988  graphic_suffix[u'episodeimagedir'])
1989  try:
1990  result = takeScreenShot(item[u'videoFilename'], screenshot_mythvideo, size_limit=False)
1991  except:
1992  result = None
1993  if result != None:
1994  item[u'screenshot'] = screenshot_mythvideo
1995  tmp_array = createVideometadataRecord(item)
1996  videometadata = tmp_array[0]
1997  oldrecorded = tmp_array[1]
1998  if simulation:
1999  logger.info(u"Simulation: Create videometadata record for (%s - %s)" % \
2000  (item[u'channelTitle'], item[u'title']))
2001  else: # Check for duplicates
2002  if not local_only and videometadata[u'filename'][0] != u'/':
2003  intid = list(mythdb.searchVideos(exactfile=videometadata[u'filename'],
2004  host=localhostname.lower()))
2005  else:
2006  intid = list(mythdb.searchVideos(exactfile=videometadata[u'filename']))
2007 
2008  if intid == []: # Check for an empty array
2009  try:
2010  intid = Video(db=mythdb).create(videometadata).intid
2011  except MythError, e:
2012  logger.critical(u"Adding Miro video to MythVideo (%s - %s) failed for (%s)." % \
2013  (item[u'channelTitle'], item[u'title'], e.args[0]))
2014  sys.exit(1)
2015  if not item.has_key(u'copied'):
2016  try:
2017  OldRecorded().create(oldrecorded)
2018  except MythError, e:
2019  logger.critical(u"Failed writing the oldrecorded record for(%s)" % (e.args[0]))
2020  sys.exit(1)
2021  if videometadata[u'filename'][0] == u'/':
2022  cmd = mythcommflag_videos % videometadata[u'filename']
2023  elif videometadata[u'host'] and storagegroups[u'mythvideo']:
2024  cmd = mythcommflag_videos % \
2025  ((storagegroups[u'mythvideo']+videometadata[u'filename']))
2026  statistics[u'Miros_MythVideos_added']+=1
2027  statistics[u'Total_Miro_expiring']+=1
2028  statistics[u'Total_Miro_MythVideos']+=1
2029  displayMessage(u"Added Miro video to MythVideo (%s - %s)" % \
2030  (videometadata[u'title'], videometadata[u'subtitle']))
2031  else:
2032  sys.stdout.write(u'')
2033  displayMessage(\
2034 u"""Skipped adding a duplicate Miro video to MythVideo:
2035 (%s - %s)
2036 Sometimes a Miro channel has the same video downloaded multiple times.
2037 This is a Miro/Channel web site issue and often rectifies itself overtime.
2038 """ % (videometadata[u'title'], videometadata[u'subtitle']))
2039 
2040  return True
2041  # end updateMythVideo()
2042 
2044  global statistics
2045 
2046  # Print statistics
2047  sys.stdout.write(u"""
2048 
2049 -------------------Statistics--------------------
2050 Number of Watch Recording's watched...... (% 5d)
2051 Number of Miro videos marked as seen..... (% 5d)
2052 Number of Miro videos deleted............ (% 5d)
2053 Number of New Miro videos downloaded..... (% 5d)
2054 Number of Miro/MythVideo's removed....... (% 5d)
2055 Number of Miro/MythVideo's added......... (% 5d)
2056 Number of Miro videos copies to MythVideo (% 5d)
2057 -------------------------------------------------
2058 Total Unwatched Miro/Watch Recordings.... (% 5d)
2059 Total Miro/MythVideo videos to expire.... (% 5d)
2060 Total Miro/MythVideo videos.............. (% 5d)
2061 -------------------------------------------------
2062 
2063 """ % (statistics[u'WR_watched'], statistics[u'Miro_marked_watch_seen'],
2064  statistics[u'Miro_videos_deleted'], statistics[u'Miros_videos_downloaded'],
2065  statistics[u'Miros_MythVideos_video_removed'], statistics[u'Miros_MythVideos_added'],
2066  statistics[u'Miros_MythVideos_copied'], statistics[u'Total_unwatched'],
2067  statistics[u'Total_Miro_expiring'], statistics[u'Total_Miro_MythVideos'], ))
2068  # end printStatistics()
2069 
2070 
2071 # Main script processing starts here
2072 def main():
2073  """Support mirobridge from the command line
2074  returns True
2075  """
2076  global localhostname, simulation, verbose, storagegroups, ffmpeg, channel_id, channel_num
2077  global flat, download_sleeptime, channel_watch_only, channel_mythvideo_only, channel_new_watch_copy
2078  global vid_graphics_dirs, imagemagick, statistics, requirements_are_met
2079  global graphic_suffix, graphic_path_suffix, graphic_name_suffix
2080  global mythcommflag_recordings, mythcommflag_videos
2081  global local_only, metadata
2082  global parser, opts, args
2083 
2084  if opts.examples: # Display example information
2085  sys.stdout.write(examples_txt+'\n')
2086  sys.exit(0)
2087 
2088  if opts.version: # Display program information
2089  sys.stdout.write(u"\nTitle: (%s); Version: description(%s); Author: (%s)\n%s\n" % (
2090  __title__, __version__, __author__, __purpose__ ))
2091  sys.exit(0)
2092 
2093  if opts.testenv:
2094  test_environment = True
2095  else:
2096  test_environment = False
2097 
2098  # Verify that Miro is not currently running
2099  if isMiroRunning():
2100  sys.exit(1)
2101 
2102  # Verify that only None or one of the mutually exclusive (-W), (-M) and (-N) options is being used
2103  x = 0
2104  if opts.new_watch_copy: x+=1
2105  if opts.watch_only: x+=1
2106  if opts.mythvideo_only: x+=1
2107  if opts.import_opml: x+=1
2108  if x > 1:
2109  logger.critical(u"The (-W), (-M), (-N) and (-i) options are mutually exclusive, "\
2110  u"so only one can be specified at a time.")
2111  sys.exit(1)
2112 
2113  # Set option related global variables
2114  simulation = opts.simulation
2115  verbose = opts.verbose
2116  if opts.hostname: # Override localhostname if the user specified an hostname
2117  localhostname = opts.hostname
2118 
2119  # Validate settings
2120 
2121  ## Video base directory and current version and revision numbers
2122  base_video_dir = miroConfiguration(prefs.MOVIES_DIRECTORY)
2123  miro_version_rev = u"%s r%s" % (miroConfiguration(prefs.APP_VERSION),
2124  miroConfiguration(prefs.APP_REVISION_NUM))
2125 
2126  displayMessage(u"Miro Version (%s)" % (miro_version_rev))
2127  displayMessage(u"Base Miro Video Directory (%s)" % (base_video_dir,))
2128  logger.info(u'')
2129 
2130  # Verify Miro version sufficent and Video file configuration correct.
2131  if not os.path.isdir(base_video_dir):
2132  logger.critical(u"The Miro Videos directory (%s) does not exist." % str(base_video_dir))
2133  if test_environment:
2134  requirements_are_met = False
2135  else:
2136  sys.exit(1)
2137 
2138  if miroConfiguration(prefs.APP_VERSION) < u"2.0.3":
2139  logger.critical((u"The installed version of Miro (%s) is too old. It must be at least "\
2140  u"v2.0.3 or higher.") % miroConfiguration(prefs.APP_VERSION))
2141  if test_environment:
2142  requirements_are_met = False
2143  else:
2144  sys.exit(1)
2145 
2146  # Miro 4.0.1 has a critical bug that effects MiroBridge. Miro must be upgraded.
2147  if miroConfiguration(prefs.APP_VERSION) == u"4.0.1":
2148  logger.critical((u"The installed version of Miro (%s) must be upgraded to Miro version "\
2149  u"4.0.2 or higher.") % miroConfiguration(prefs.APP_VERSION))
2150  if test_environment:
2151  requirements_are_met = False
2152  else:
2153  sys.exit(1)
2154 
2155  # Verify that the import opml option can be used
2156  if opts.import_opml:
2157  if miroConfiguration(prefs.APP_VERSION) < u"2.5.2":
2158  logger.critical(u"The OPML import option requires Miro v2.5.2 or higher your Miro "\
2159  u"(%s) is too old." % miroConfiguration(prefs.APP_VERSION))
2160  if test_environment:
2161  requirements_are_met = False
2162  else:
2163  sys.exit(1)
2164  if not os.path.isfile(opts.import_opml):
2165  logger.critical(u"The OPML import file (%s) does not exist" % opts.import_opml)
2166  if test_environment:
2167  requirements_are_met = False
2168  else:
2169  sys.exit(1)
2170  if len(opts.import_opml) > 5:
2171  if not opts.import_opml[:-4] != '.opml':
2172  logger.critical(u"The OPML import file (%s) must had a file extention of '.opml'" % \
2173  opts.import_opml)
2174  if test_environment:
2175  requirements_are_met = False
2176  else:
2177  sys.exit(1)
2178  else:
2179  logger.critical(u"The OPML import file (%s) must had a file extention of '.opml'" % \
2180  opts.import_opml)
2181  if test_environment:
2182  requirements_are_met = False
2183  else:
2184  sys.exit(1)
2185 
2186  # Get storage groups
2187  if getStorageGroups() == False:
2188  logger.critical(u"Retrieving storage groups from the MythTV data base failed")
2189  if test_environment:
2190  requirements_are_met = False
2191  else:
2192  sys.exit(1)
2193  elif not u'default' in storagegroups.keys():
2194  logger.critical(u"There must be a 'Default' storage group")
2195  if test_environment:
2196  requirements_are_met = False
2197  else:
2198  sys.exit(1)
2199 
2200  if opts.channel:
2201  channel = opts.channel.split(u':')
2202  if len(channel) != 2:
2203  logger.critical((u"The Channel (%s) must be in the format xxx:yyy with x and "\
2204  u"y all numeric.") % str(opts.channel))
2205  if test_environment:
2206  requirements_are_met = False
2207  else:
2208  sys.exit(1)
2209  elif not _can_int(channel[0]) or not _can_int(channel[1]):
2210  logger.critical(u"The Channel_id (%s) and Channel_num (%s) must be numeric." % \
2211  (channel[0], channel[1]))
2212  if test_environment:
2213  requirements_are_met = False
2214  else:
2215  sys.exit(1)
2216  else:
2217  channel_id = int(channel[0])
2218  channel_num = int(channel[1])
2219 
2220  if opts.sleeptime:
2221  if not _can_int(opts.sleeptime):
2222  logger.critical(u"Auto-download sleep time (%s) must be numeric." % str(opts.sleeptime))
2223  if test_environment:
2224  requirements_are_met = False
2225  else:
2226  sys.exit(1)
2227  else:
2228  download_sleeptime = float(opts.sleeptime)
2229 
2230  getMythtvDirectories() # Initialize all the Video and graphics directory dictionary
2231 
2232  if opts.nosubdirs: # Did the user want a flat MythVideo "Miro" directory structure?
2233  flat = True
2234 
2235  # Get the values in the mirobridge.conf configuration file
2236  setUseroptions()
2237 
2238  if opts.watch_only:
2239  # ALL Miro videos will only be viewed in the MythTV "Watch Recordings" screen
2240  channel_watch_only = [u'all']
2241 
2242  if opts.mythvideo_only: # ALL Miro videos will be copied to MythVideo and removed from Miro
2243  channel_mythvideo_only = {u'all': vid_graphics_dirs[u'mythvideo']+u'Miro/'}
2244 
2245  # Once watched ALL Miro videos will be copied to MythVideo and removed from Miro
2246  if opts.new_watch_copy:
2247  channel_new_watch_copy = {u'all': vid_graphics_dirs[u'mythvideo']+u'Miro/'}
2248 
2249  # Verify that "Mythvideo Only" and "New-Watch-Copy" channels do not clash
2250  if len(channel_mythvideo_only) and len(channel_new_watch_copy):
2251  for key in channel_mythvideo_only.keys():
2252  if key in channel_new_watch_copy.keys():
2253  logger.critical((u'The Miro Channel (%s) cannot be used as both a "Mythvideo '\
2254  u'Only" and "New-Watch-Copy" channel.') % key)
2255  if test_environment:
2256  requirements_are_met = False
2257  else:
2258  sys.exit(1)
2259 
2260  # Verify that ImageMagick is installed
2261  ret = useImageMagick(u"convert -version")
2262  if ret < 0 or ret > 1:
2263  logger.critical(u"ImageMagick must be installed, graphics cannot be resized or "\
2264  u"converted to the required graphics format (e.g. jpg and or png)")
2265  if test_environment:
2266  requirements_are_met = False
2267  else:
2268  sys.exit(1)
2269 
2270  # Verify that the "DeletesFollowLinks" setting is not set to the character '1'
2271  if mythdb.settings.NULL.DeletesFollowLinks == '1':
2272  logger.critical(u'The MythTV back end setting "Follow symbolic links when deleting '\
2273  u'files" is checked and it is incompatible with MiroBridge processing. '\
2274  u'It must be unchecked it to use MiroBridge.\nTo uncheck this setting '\
2275  u'start "mythtv-setup" or with Mythbuntu start "MythTV Backend Setup" '\
2276  u'and then General->Miscellaneous Settings and uncheck the "Follow '\
2277  u'symbolic links when deleting files" setting')
2278  if test_environment:
2279  requirements_are_met = False
2280  else:
2281  sys.exit(1)
2282 
2283  # Initialize class with the metadata methods
2284  metadata = MetaData(mythdb,
2285  Video,
2286  Record,
2287  storagegroups,
2288  vid_graphics_dirs,
2289  channel_id,
2290  ffmpeg,
2291  logger,
2292  simulation,
2293  verbose,
2294  )
2295 
2296  if opts.testenv: # All tests passed
2297  metadata.getVideoDetails(u"") # Test that ffmpeg is available
2298  if ffmpeg and requirements_are_met:
2299  logger.info(u"The environment test passed !\n\n")
2300  sys.exit(0)
2301  else:
2302  logger.critical(u"The environment test FAILED. See previously displayed error messages!")
2303  sys.exit(1)
2304 
2305  if opts.addchannel != u'OFF': # Add a Miro Channel record - Should only be done once
2306  createChannelRecord(opts.addchannel, channel_id, channel_num)
2307  logger.info(u"The Miro Channel record has been successfully created !\n\n")
2308  sys.exit(0)
2309 
2310 
2311  ###########################################
2312  # Mainlogic for all Miro bridge and MythTV
2313  ###########################################
2314 
2315  #
2316  # Start the Miro Front and Backend - This allows mirobridge to execute actions on the Miro backend
2317  #
2318  displayMessage(u"Starting Miro Frontend and Backend")
2319  startup.initialize(miroConfiguration(prefs.THEME_NAME))
2320  if miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4
2321  app.info_updater = InfoUpdater()
2322  app.cli_events = EventHandler()
2323  app.cli_events.connect_to_signals()
2324 
2325  if miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4
2326  startup.install_first_time_handler(app.cli_events.handle_first_time)
2327 
2328  startup.startup()
2329  app.cli_events.startup_event.wait()
2330  if app.cli_events.startup_failure:
2331  logger.critical(u"Starting Miro Frontend and Backend failed: (%s)\n(%s)" % \
2332  (app.cli_events.startup_failure[0], app.cli_events.startup_failure[1]))
2333  app.controller.shutdown()
2334  time.sleep(5) # Let the shutdown processing complete
2335  sys.exit(1)
2336 
2337  if miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4
2338  app.movie_data_program_info = movie_data_program_info
2339  messages.FrontendStarted().send_to_backend()
2340 
2341  app.cli_interpreter = MiroInterpreter()
2342  if opts.verbose:
2343  app.cli_interpreter.verbose = True
2344  else:
2345  app.cli_interpreter.verbose = False
2346  app.cli_interpreter.simulation = opts.simulation
2347  app.cli_interpreter.videofiles = []
2348  app.cli_interpreter.downloading = False
2349  app.cli_interpreter.icon_cache_dir = miroConfiguration(prefs.ICON_CACHE_DIRECTORY)
2350  app.cli_interpreter.imagemagick = imagemagick
2351  app.cli_interpreter.statistics = statistics
2352  if miroConfiguration(prefs.APP_VERSION) < u"2.5.0":
2353  app.renderer = app.cli_interpreter
2354  elif miroConfiguration(prefs.APP_VERSION) > u"4.0": # Only required for Miro 4
2355  pass
2356  else:
2357  app.movie_data_program_info = app.cli_interpreter.movie_data_program_info
2358 
2359  #
2360  # Attempt to import an opml file
2361  #
2362  if opts.import_opml:
2363  results = 0
2364  try:
2365  app.cli_interpreter.do_mythtv_import_opml(opts.import_opml)
2366  time.sleep(30) # Let the Miro backend process the OPML file before shutting down
2367  except Exception, e:
2368  logger.critical(u"Import of OPML file (%s) failed, error(%s)." % (opts.import_opml, e))
2369  results = 1
2370  # Gracefully close the Miro database and shutdown the Miro Front and Back ends
2371  app.controller.shutdown()
2372  time.sleep(5) # Let the shutdown processing complete
2373  sys.exit(results)
2374 
2375  #
2376  # Optionally Update Miro feeds and
2377  # download any "autodownloadable" videos which are pending
2378  #
2379  if not opts.no_autodownload:
2380  if opts.verbose:
2381  app.cli_interpreter.verbose = False
2382  app.cli_interpreter.do_mythtv_getunwatched(u'')
2383  before_download = len(app.cli_interpreter.videofiles)
2384  if opts.verbose:
2385  app.cli_interpreter.verbose = True
2386  if miroConfiguration(prefs.APP_VERSION) < u"4.0":
2387  # Miro 4 automatically refreshes feeds and downloads
2388  app.cli_interpreter.do_mythtv_update_autodownload(u'')
2389  time.sleep(download_sleeptime)
2390  firsttime = True
2391  while True:
2392  app.cli_interpreter.do_mythtv_check_downloading(u'')
2393  if app.cli_interpreter.downloading:
2394  time.sleep(30)
2395  firsttime = False
2396  continue
2397  elif firsttime:
2398  time.sleep(download_sleeptime)
2399  firsttime = False
2400  continue
2401  else:
2402  break
2403  if opts.verbose:
2404  app.cli_interpreter.verbose = False
2405  app.cli_interpreter.do_mythtv_getunwatched(u'')
2406  after_download = len(app.cli_interpreter.videofiles)
2407  statistics[u'Miros_videos_downloaded'] = after_download - before_download
2408  if opts.verbose:
2409  app.cli_interpreter.verbose = True
2410 
2411  # Deal with orphaned oldrecorded records.
2412  # These records indicate that the MythTV user deleted the video from the Watched Recordings screen
2413  # or from MythVideo
2414  # These video items must also be deleted from Miro
2415  videostodelete = getOldrecordedOrphans()
2416  if len(videostodelete):
2417  displayMessage(u"Starting Miro delete of videos deleted in the MythTV "\
2418  u"Watched Recordings screen.")
2419  for video in videostodelete:
2420  # Completely remove the video and item information from Miro
2421  app.cli_interpreter.do_mythtv_item_remove([video[u'title'], video[u'subtitle']])
2422 
2423  #
2424  # Collect the set of played Miro video files
2425  #
2426  app.cli_interpreter.videofiles = getPlayedMiroVideos()
2427 
2428  #
2429  # Updated the played status of items
2430  #
2431  if app.cli_interpreter.videofiles:
2432  displayMessage(u"Starting Miro update of watched MythTV videos")
2433  app.cli_interpreter.do_mythtv_updatewatched(u'')
2434 
2435  #
2436  # Get the unwatched videos details from Miro
2437  #
2438  app.cli_interpreter.do_mythtv_getunwatched(u'')
2439  unwatched = app.cli_interpreter.videofiles
2440 
2441  #
2442  # Get the watched videos details from Miro
2443  #
2444  app.cli_interpreter.do_mythtv_getwatched(u'')
2445  watched = app.cli_interpreter.videofiles
2446 
2447  #
2448  # Massage empty titles and subtitles from Miro
2449  #
2450  for item in unwatched:
2451  # Deal with empty titles and subtitles from Miro
2452  if not item[u'channelTitle']:
2453  item[u'channelTitle'] = emptyTitle
2454  if not item[u'title']:
2455  item[u'title'] = emptySubTitle
2456  for item in watched:
2457  # Deal with empty titles and subtitles from Miro
2458  if not item[u'channelTitle']:
2459  item[u'channelTitle'] = emptyTitle
2460  if not item[u'title']:
2461  item[u'title'] = emptySubTitle
2462 
2463  #
2464  # Remove any duplicate Miro videoes from the unwatched or watched list of Miro videos
2465  # This means that Miro has duplicates due to a Miro/Channel website issue
2466  # These videos should not be added to the MythTV Watch Recordings screen
2467  #
2468  unwatched_copy = []
2469  for item in unwatched:
2470  unwatched_copy.append(item)
2471  for item in unwatched_copy: # Check for a duplicate against already watched Miro videos
2472  for x in watched:
2473  if item[u'channelTitle'] == x[u'channelTitle'] and item[u'title'] == x[u'title']:
2474  try:
2475  unwatched.remove(item)
2476  # Completely remove this duplicate video and item information from Miro
2477  app.cli_interpreter.do_mythtv_item_remove(item[u'videoFilename'])
2478  displayMessage((u"Skipped adding a duplicate Miro video to the MythTV "\
2479  u"Watch Recordings screen (%s - %s) which is already in "\
2480  u"MythVideo.\nSometimes a Miro channel has the same video "\
2481  u"downloaded multiple times.\nThis is a Miro/Channel web "\
2482  u"site issue and often rectifies itself overtime.") % \
2483  (item[u'channelTitle'], item[u'title']))
2484  except ValueError:
2485  pass
2486  duplicates = []
2487  for item in unwatched_copy:
2488  dup_flag = 0
2489  for x in unwatched: # Check for a duplicate against un-watched Miro videos
2490  if item[u'channelTitle'] == x[u'channelTitle'] and item[u'title'] == x[u'title']:
2491  dup_flag+=1
2492  if dup_flag > 1:
2493  for x in duplicates:
2494  if item[u'channelTitle'] == x[u'channelTitle'] and item[u'title'] == x[u'title']:
2495  break
2496  else:
2497  duplicates.append(item)
2498 
2499  for duplicate in duplicates:
2500  try:
2501  unwatched.remove(duplicate)
2502  # Completely remove this duplicate video and item information from Miro
2503  app.cli_interpreter.do_mythtv_item_remove(duplicate[u'videoFilename'])
2504  displayMessage((u"Skipped adding a Miro video to the MythTV Watch Recordings "\
2505  u"screen (%s - %s) as there are duplicate 'new' video items."\
2506  u"\nSometimes a Miro channel has the same video downloaded "\
2507  u"multiple times.\nThis is a Miro/Channel web site issue and "\
2508  u"often rectifies itself overtime.") % \
2509  (duplicate[u'channelTitle'], duplicate[u'title']))
2510  except ValueError:
2511  pass
2512 
2513  #
2514  # Deal with any Channel videos that are to be copied and removed from Miro
2515  #
2516  copy_items = []
2517  # Copy unwatched and watched Miro videos (all or only selected Channels)
2518  if u'all' in channel_mythvideo_only.keys():
2519  for array in [watched, unwatched]:
2520  for item in array:
2521  copy_items.append(item)
2522  elif len(channel_mythvideo_only):
2523  for array in [watched, unwatched]:
2524  for video in array:
2525  if filter(is_not_punct_char, video[u'channelTitle'].lower()) in \
2526  channel_mythvideo_only.keys():
2527  copy_items.append(video)
2528  # Copy ONLY watched Miro videos (all or only selected Channels)
2529  if u'all' in channel_new_watch_copy.keys():
2530  for video in watched:
2531  copy_items.append(video)
2532  elif len(channel_new_watch_copy):
2533  for video in watched:
2534  if filter(is_not_punct_char, video[u'channelTitle'].lower()) in \
2535  channel_new_watch_copy.keys():
2536  copy_items.append(video)
2537 
2538  channels_to_copy = {}
2539  for key in channel_mythvideo_only.keys():
2540  channels_to_copy[key] = channel_mythvideo_only[key]
2541  for key in channel_new_watch_copy.keys():
2542  channels_to_copy[key] = channel_new_watch_copy[key]
2543 
2544  for video in copy_items:
2545  if channels_to_copy.has_key('all'):
2546  copy_dir = u"%s%s/" % (channels_to_copy['all'], sanitiseFileName(video[u'channelTitle']))
2547  else:
2548  copy_dir = channels_to_copy[filter(is_not_punct_char, video[u'channelTitle'].lower())]
2549 
2550  # Create the subdirectories to copy the video into
2551  if not os.path.isdir(copy_dir):
2552  if simulation:
2553  logger.info(u"Simulation: Creating the MythVideo directory (%s)." % (copy_dir))
2554  else:
2555  os.makedirs(copy_dir)
2556 
2557  # Copy the Miro video file
2558  # This filename is needed later for deleting in Miro
2559  save_video_filename = video[u'videoFilename']
2560  ext = getExtention(video[u'videoFilename'])
2561  if ext.lower() == u'm4v':
2562  ext = u'mpg'
2563  filepath = u"%s%s - %s.%s" % (copy_dir, sanitiseFileName(video[u'channelTitle']),
2564  sanitiseFileName(video[u'title']), ext)
2565  if simulation:
2566  logger.info(u"Simulation: Copying the Miro video (%s) to the MythVideo directory (%s)." % \
2567  (video[u'videoFilename'], filepath))
2568  else:
2569  try: # Miro video copied into a MythVideo directory
2570  shutil.copy2(video[u'videoFilename'], filepath)
2571  statistics[u'Miros_MythVideos_copied']+=1
2572  if u'mythvideo' in storagegroups.keys() and not local_only:
2573  video[u'videoFilename'] = filepath.replace(storagegroups[u'mythvideo'], u'')
2574  else:
2575  video[u'videoFilename'] = filepath
2576  except Exception, e:
2577  logger.critical((u"Copying the Miro video (%s) to the MythVideo directory (%s)."\
2578  u"\n This maybe a permissions error (mirobridge.py does "\
2579  u"not have permission to write to the directory), error(%s)") % \
2580  (video[u'videoFilename'], filepath, e))
2581  # Gracefully close the Miro database and shutdown the Miro Front and Back ends
2582  app.controller.shutdown()
2583  time.sleep(5) # Let the shutdown processing complete
2584  sys.exit(1)
2585 
2586  # Copy the Channel or item's icon
2587  if video[u'channel_icon'] and not video[u'channelTitle'].lower() in channel_icon_override:
2588  pass
2589  else:
2590  if video[u'item_icon']:
2591  video[u'channel_icon'] = video[u'item_icon']
2592  # Get any graphics that already exist
2593  graphics = metadata.getMetadata(sanitiseFileName(video[u'channelTitle']))
2594  if video[u'channel_icon'] and graphics['coverart'] == u'':
2595  ext = getExtention(video[u'channel_icon'])
2596  if video[u'channelTitle'].lower() in channel_icon_override:
2597  filepath = u"%s%s - %s%s.%s" % (vid_graphics_dirs[u'posterdir'],
2598  sanitiseFileName(video[u'channelTitle']),
2599  sanitiseFileName(video[u'title']),
2600  graphic_suffix[u'posterdir'], ext)
2601  else:
2602  filepath = u"%s%s%s.%s" % (vid_graphics_dirs[u'posterdir'],
2603  sanitiseFileName(video[u'channelTitle']),
2604  graphic_suffix[u'posterdir'], ext)
2605  # There may already be a Channel icon available
2606  # or it is a symlink which needs to be replaced
2607  if not os.path.isfile(filepath) or os.path.islink(filepath):
2608  if simulation:
2609  logger.info((u"Simulation: Copying the Channel Icon (%s) to the poster "\
2610  u"directory (%s).") % (video[u'channel_icon'], filepath))
2611  else:
2612  try: # Miro Channel icon copied into a MythVideo directory
2613  try: # Remove any old symlink file
2614  os.remove(filepath)
2615  except OSError:
2616  pass
2617  shutil.copy2(video[u'channel_icon'], filepath)
2618  if u'posterdir' in storagegroups.keys() and not local_only:
2619  video[u'channel_icon'] = filepath.replace(storagegroups[u'posterdir'], u'')
2620  else:
2621  video[u'channel_icon'] = filepath
2622  except Exception, e:
2623  logger.critical((u"Copying the Channel Icon (%s) to the poster directory "\
2624  u"(%s).\n This maybe a permissions error "\
2625  u"(mirobridge.py does not have permission to write to the "\
2626  u"directory), error(%s)") % \
2627  (video[u'channel_icon'], filepath, e))
2628  # Gracefully close the Miro database and shutdown the Miro Front and Back ends
2629  app.controller.shutdown()
2630  time.sleep(5) # Let the shutdown processing complete
2631  sys.exit(1)
2632  else:
2633  if u'posterdir' in storagegroups.keys() and not local_only:
2634  video[u'channel_icon'] = filepath.replace(storagegroups[u'posterdir'], u'')
2635  else:
2636  video[u'channel_icon'] = filepath
2637  else:
2638  video[u'channel_icon'] = graphics['coverart']
2639 
2640  # There may already be a Screenshot available or it is a symlink which needs to be replaced
2641  if video[u'screenshot']:
2642  ext = getExtention(video[u'screenshot'])
2643  filepath = u"%s%s - %s%s.%s" % (vid_graphics_dirs[u'episodeimagedir'],
2644  sanitiseFileName(video[u'channelTitle']),
2645  sanitiseFileName(video[u'title']),
2646  graphic_suffix[u'episodeimagedir'], ext)
2647  else:
2648  filepath = u''
2649 
2650  if not os.path.isfile(filepath) or os.path.islink(filepath):
2651  if video[u'screenshot']:
2652  if simulation:
2653  logger.info((u"Simulation: Copying the Screenshot (%s) to the Screenshot "\
2654  u"directory (%s).") % (video[u'screenshot'], filepath))
2655  else:
2656  try: # Miro Channel icon copied into a MythVideo directory
2657  try: # Remove any old symlink file
2658  os.remove(filepath)
2659  except OSError:
2660  pass
2661  shutil.copy2(video[u'screenshot'], filepath)
2662  displayMessage(u"Copied Miro screenshot file (%s) to MythVideo (%s)" % \
2663  (video[u'screenshot'], filepath))
2664  if u'episodeimagedir' in storagegroups.keys() and not local_only:
2665  video[u'screenshot'] = filepath.replace(storagegroups[u'episodeimagedir'],
2666  u'')
2667  else:
2668  video[u'screenshot'] = filepath
2669  except Exception, e:
2670  logger.critical((u"Copying the Screenshot (%s) to the Screenshot directory "\
2671  u"(%s).\n This maybe a permissions error "\
2672  u"(mirobridge.py does not have permission to write to the "\
2673  u"directory), error(%s)") % \
2674  (video[u'screenshot'], filepath, e))
2675  # Gracefully close the Miro database and shutdown the Miro Front and Back ends
2676  app.controller.shutdown()
2677  time.sleep(5) # Let the shutdown processing complete
2678  sys.exit(1)
2679  elif video[u'screenshot']:
2680  if u'episodeimagedir' in storagegroups.keys() and not local_only:
2681  video[u'screenshot'] = filepath.replace(storagegroups[u'episodeimagedir'], u'')
2682  else:
2683  video[u'screenshot'] = filepath
2684  video[u'copied'] = True # Mark this video item as being copied
2685 
2686  # Completely remove the video and item information from Miro
2687  app.cli_interpreter.do_mythtv_item_remove(save_video_filename)
2688 
2689 
2690  # Gracefully close the Miro database and shutdown the Miro Front and Back ends
2691  app.controller.shutdown()
2692  time.sleep(5) # Let the shutdown processing complete
2693 
2694  #
2695  # Add and delete MythTV (Watch Recordings) Miro recorded records
2696  # Add and remove symlinks for Miro video files
2697  #
2698 
2699  # Check if the user does not want any channels Added to the "Watch Recordings" screen
2700  if channel_mythvideo_only.has_key(u'all'):
2701  for video in unwatched:
2702  watched.append(video)
2703  unwatched = []
2704  else:
2705  if len(channel_mythvideo_only):
2706  unwatched_copy = []
2707  for video in unwatched:
2708  if not filter(is_not_punct_char, video[u'channelTitle'].lower()) in \
2709  channel_mythvideo_only.keys():
2710  unwatched_copy.append(video)
2711  else:
2712  watched.append(video)
2713  unwatched = unwatched_copy
2714 
2715  statistics[u'Total_unwatched'] = len(unwatched)
2716  if not len(unwatched):
2717  displayMessage(u"There are no Miro unwatched video items to add as MythTV Recorded videos.")
2718  if not updateMythRecorded(unwatched):
2719  logger.critical(u"Updating MythTV Recording with Miro video files failed." % \
2720  str(base_video_dir))
2721  sys.exit(1)
2722 
2723  #
2724  # Add and delete MythVideo records for played Miro Videos
2725  # Add and delete symbolic links to Miro Videos and subdirectories
2726  # Add and delete symbolic links to coverart/Miro icons and Miro screenshots/fanart
2727  #
2728  if len(channel_watch_only): # If the user does not want any channels moved to MythVideo exit
2729  if channel_watch_only[0].lower() == u'all':
2730  printStatistics()
2731  return True
2732 
2733  if not len(watched):
2734  displayMessage(u"There are no Miro watched items to add to MythVideo")
2735  if not updateMythVideo(watched):
2736  logger.critical(u"Updating MythVideo with Miro video files failed.")
2737  sys.exit(1)
2738 
2739  printStatistics()
2740  return True
2741 # end main
2742 
2743 if __name__ == "__main__":
2744  myapp = singleinstance(u'/tmp/mirobridge.pid')
2745  #
2746  # check is another instance of Miro Bridge running
2747  #
2748  if myapp.alreadyrunning():
2749  print u'\nMiro Bridge is already running only one instance can run at a time\n\n'
2750  sys.exit(0)
2751 
2752  main()
2753  displayMessage(u"Miro Bridge Processing completed")
2754