#!/usr/bin/env python # -*- coding: UTF-8 -*- # ---------------------- # Name: jamu.py Just.Another.Metadata.Utility # Python Script # Author: R.D. Vaughan # Purpose: This python script is intended to perform a variety of utility functions on mythvideo # metadata and the associated video files. # # The primary movie source for graphics and data is themoviedb.com wiki. # The primary TV Series source for graphics and data is thetvdb.com wiki. # Users of this script are encouraged to populate both themoviedb.com and thetvdb.com # with posters, fan art and banners and meta data. The richer the source the more valuable # the script. # This script uses the python module tvdb_api.py (v0.6DEV or higher) found at # http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search thanks # to the authors of this excellent module. # The tvdb_api.py module uses the full access XML api published by thetvdb.com see: # http://thetvdb.com/wiki/index.php?title=Programmers_API # This python script's functionality is enhanced if you have installed "tvnamer.py" created by # "dbr/Ben" who is also the author of the "tvdb_api.py" module. # "tvnamer.py" is used to rename avi files with series/episode information found at # thetvdb.com # Python access to the tmdb api started with a module from dbr/Ben and then enhanced for # Jamu's needs. # The routines to select video files was copied and modified from tvnamer.py mentioned above. # The routine "_save_video_metadata_to_mythdb" has been taken and modified from # "find_meta.py" author Pekka Jääskeläinen. # The routine "_addCastGenre" was taken and modified from "tvdb-bulk-update.py" by # author David Shilvock . # # Command line examples: # See help (-u and -h) options # # License:Creative Commons GNU GPL v2 # (http://creativecommons.org/licenses/GPL/2.0/) #------------------------------------- __title__ ="JAMU - Just.Another.Metadata.Utility"; __author__="R.D.Vaughan" __purpose__=''' This python script is intended to perform a variety of utility functions on mythvideo metadata and the associated video files. The primary movie source for graphics and data is themoviedb.com wiki. The primary TV Series source for graphics and data is thetvdb.com wiki. Users of this script are encouraged to populate both themoviedb.com and thetvdb.com with posters, fan art and banners and meta data. The richer the source the more valuable the script. ''' __version__=u"v0.7.3" # 0.1.0 Initial development # 0.2.0 Inital beta release # 0.3.0 Add mythvideo metadata updating including movie graphics through # the use of tmdb.pl when the perl script exists # 0.3.1 Add mythvideo meta data add and update functionality. Intend use for # maintenance cron jobs. # Increase integration with mythtvideo download meta data and MythUI # Added the ability to movie video files while maintaining the metadata # 0.3.2 Fixed bug where some poster downloads were unnecessary # Fixed bug where the mythtv database was updated for no reason # Fixed bug in jamu-example.conf "min_poster_size" variable had '=' not ':' # Fixed bug where a unicode URL would abort the script # Using ffmpeg added setting accurate video length in minutes. A hack but # lacked python method to find audio/video properties. # 0.3.3 Add logic to skip any video with a inetref of '99999999'. Meta data and # graphics are all manually entered and should not be altered by Jamu. # Currently used for any meta data that you do not want modified by Jamu. # Fixed issues with filenames containing Unicode characters. # 0.3.4 Added logic to skip any secondary source meta data plot less than 10 words. # Properly initialized a new record so warning messages do not display. # In plot meta data replace line-feeds with a space (e.g. Space Cowboys # plot contains line-feeds). Mythvideo does not expect line-feeds in a plot. # Significant improvements in combining meta data between primary and # secondary data sources. # Remove 'tmdb.pl' calls and use the tmdb api directly. # Added detection of broken symbolic links and fixed those links. # Fixed inconsistencies in graphics file extentions (as received from the # sources), made all extentions lowercase and changed ".jpeg" to ".jpg". # 0.3.5 Fixed bug when themoviedb.com times out from an api request. # A few documentation corrections. # Fixed a bug with utf8 directory names. # Added code to not abort script when themoviedb.com has problems. The issue # is reported but the scripts continues processing. # Added option "-W" to download graphics for Scheduled and Recorded videos. # Change the "-J" Janitor function to avoid deleting graphics for Scheduled # and Recorded videos. # Fixed bug where a TMDB Poster image was not found when it was really # available. # 0.3.6 Fixed bug when searching themoviedb.com for a movie by title or # alternate title. # Increased accuracy of non-interactive TMDB movie searching and matching. # Set up for transition to TMDB's beta v2.1 api which adds language support. # Corrected Watched Recording graphic file naming convention for movies. # If interactive mode is selected but an exact match is found for a movie # then the exact match is chosen and no interative session is initiated. # Added additional messages when access to MythTV python bindings has issues. # 0.3.7 Removed some redundant code. # Sync up with v1.0 of tvdb_api and new way to assign tvdb api key # Added an option (-MG) to allow Jamu best guessing at a video's inetref # number. To guess accurately the video file name must be very close to # those found on tmdb or imdb and tvdb web sites. # Remove all use of the MythVideo.py "pruneMetadata" routine as it deletes # records from the Mythvideo table for all video files with relative file # paths. # Jamu will skip processing any videometadata which is using a Storage group. # Jamu will now restrict itself to updating only videometadata records whose # video files reside on the current host machine. In the case where a user # has multiple backends jamu must run on each of those backends. # The Janitor option (-MJ) now checks if the users has set the plugins # MythGallery, MythGame and MythMusic to use the same graphics directories as # MythVideo. If they share directories the Janitor option will exit # without removing any graphics files. Messages indicating which directories # are in conflict will be displayed. # Added the detection of video or graphics on an NFS mount exiting jamu without # any processing and displaying a message why this has been done. A new option # for NFS (-MN) will allow a user to override this check and jamu will continue # processing. # Fixed a bug when TMDB does not have a 'year' for a movie (e.g. 'Bambi') # Added compatibility with or without the MythTV.py Ticket #6678 # Fixed a bug when ffmpeg cannot find the true length in minutes of a video # Cleaned up documenation consistency with Warning and Error messages. # Added to the existing TV episode video file renaming (-MF) option. # Now movie video files can also be renamed to the format "title (year)" # e.g. "The Duchess (2008)". If tmdb.com has no year for the movie then only # the movie title will be used when renaming. Any existing metadata is # preserved. # 0.3.8 Made changes to sync up with MythTV trunk change set [r21138]. # Now handles TVDB's change from a 5 digit inetref number to 6 digits. # 0.3.9 Check accessability (Read and Write) to directories and files before # including them in files/directories to process. # Add the ability to process Storage Groups for all Videos and graphics. # Jamu now uses MythVideo.py binding's Genre and Cast routines # Fixed a unicode bug with file paths. # Fixed a unicode bug with some URLs containing UTF8 characters. # Fixed a bug were a bad image file could avbort the script. # Changed all subdirectory cover art to a copied graphic file "folder.jpg/png" # to conform to the Storage Group standard. This also works for local subdirs. # Fixed a bug where a TV series with out a season specific poster or # banner would get repeatedly download. # 0.4.0 Removed a few lines of debugging code which should never have been left in a # distrubuted version. # Fixed the check that confirms that all Video and graphic directories are # read and writable. # Fixed a bug where under rare circumstances a graphic would be repeatedly # downloaded. # Made the installation of the python IMDbPy library manditory. # For all movies IMDB numbers will be used instead of converting to TMDB # numbers. This is done to maintain consistency with MythVideo movie inetref # numbers. # 0.4.1 Fixed an obscure video file rename (-F option) error # 0.4.2 Fixed a bug where bad data for either TMDB or TVDB would abort script # 0.4.3 Recent changes in the MythVideo UI graphic hunts (cover art and fanart) # have made Jamu's creation of "folder.xxx" graphics redundant. This # feature has been turned off in Jamu. There is a new user option # "folderart" that can reactivate this feature through the Jamu # configuration file. # 0.4.4 Changes to assist SG image hunting Jamu now adds the suffix "_coverart, # fanart, _banner, _screenshot" respectively to downloaded graphics. # With the use of a graphic suffix the requirement for unique graphics # directories is gone. The check has been removed. # 0.4.5 Fixed a bug where lowercase tv video filenames caused graphics files to # also be lowercase which can cause graphics to be downloaded twice. # Fixed a bug in graphics file name creation for a TV season. # Added checks for compatible python library versions of xml and MySQLdb # 0.4.6 Fixed a bug where a bad IMDB number in TMDB caused an abort. # 0.4.7 Fixed a bug where a 'recordedprogram' record is not properly paired with a # 'recorded' record. This results in no "airdate" information being available # and a script abort. An airdate year of u'0000' will be assumed. # Fix an abort bug when IMDB is having service problems and a list of # movies cannot be retrieved. # 0.4.8 Fixed a bug in a -MJ option check that removing graphics would not # conflict with graphic directories for non-Mythvideo plugins. # 0.4.9 Combine the video file extentions found in the "videotypes" table with those # in Jamu to avoid possible issues in the (-MJ) option and to have tighter # integration with MythVideo user file extention settings. # 0.5.0 Fixed a bug where a filename containing invalid characters caused an abort. # Such invalid filenames are now skipped with an appropriate message. # Added to the -MW option the fetching of graphics from TVDB and TMDB for # videos added by Miro Bridge to either Watched Recordings or MythVideo. # If Miro Bridge is not being used no additional processing is performed. # Two new sections ([mb_tv] and [mb_movies]) were added to the Jamu # configuration file to accomodate this new functionality. # The jamu configuration file now has a default name and location of # "~/.mythtv/jamu.conf". This can be overridden with the command line option. # This has been done so Jamu can better support Mythbuntu. # Removed code that was required until ticket #6678 was committed with # change set [21191] # Filtered out checks for video run length on iso, img ... etc potentially # large video files due to processing overhead especially on NFS mounts. # With the -MW option skip any recordings who's recgroup is "Deleted" # Fixed an abort where a TVDB TV series exists for a language but does not # have a series name in other languages. # 0.5.1 Fixed an abort when a user specifies secondary source input parameters # that cannot be parsed from the file name. This # covers secondary sources for metadata and graphics. # Fixed an abort when thetvdb.com cannot be contact due to network or # site issues. # Added detection of erroneous graphics file downloads that are actually HTML # due to source Web site issues. Jamu's (-MJ) janitor option also detects, # deletes these files and repairs the MythVideo record if necessary. # For the -MW option any downloaded graphics names will use the title of the # recorded program instead of that found on sources like TVDB and TMDB. This # resolves Watch Recordings image hunt issues when Schedule Direct uses a # different program title then is on either TVDB or TMDB. # Fixed an obscure bug where TVDB returns empty graphics URLs along with # proper URLs. The empty URLs are now ignored. # Fixed a bug when a language was specified and there were no graphics # for the specified language none were returned/downloaded. This even when # graphics for other languages were available. Now if there are no selected # language graphics English graphics are the fall back and if there are no # English graphics then any available graphics will be returned. # 0.5.2 Fixed an abort when trying to add a storage group graphics without a # proper file path. # 0.5.3 Fixed a bug where the filemarkup table is not cleaned up if Jamu renames # a Miro movie trailer video file that the user wants to keep in MythVideo. # Fixed a bug with Miro video file renaming of Miro Movie trailers # for the same movie but which had different file extentions. # 0.5.4 Conform to changeset 22104 setting of SG graphics directories to default to SG Videos if not configured. # 0.5.5 Deal with TV Series and Movie titles with a "/" forward slash in their name e.g. "Face/Off" # 0.5.6 Correct an issue when a user has a mixture of local and SG video records in MythVideo. Jamu was # adding a hostname when the video had an absolute path. This caused issues with playback. # Added more informative error messages when TMDB is returning bad xml responses. # Fixed an error in the graphic file naming convention when graphics share the same download directory. # 0.5.7 Remove the override of the TVDB graphics URL to the mirror site. See Kobe's comment: # http://forums.thetvdb.com/viewtopic.php?f=4&t=2161#p9089 # 0.5.8 The issue fixed in v0.5.5 with invalid file name creation did not fully cover TV shows It does now. # 0.5.9 Changed permissions checks on video directories to only require RW for the destination directories # involved in the move. With this change if a user requested a file rename (-F) option and the video # file does not have RW access the rename will be ignored. # Uses that have their Video directories set to access and read-only can now use Jamu. # Added a stdout display of the directories that Jamu will use for processing. This information may help # users resolve issues. The display happens ONLY when the -V (verbose) option is used. # 0.6.0 Changed The Janitor -J option to deal with graphics associated with a VIDEO_TS directory. # Stopped Jamu from processing any files in a "VIDEO_TS" directory as it was leading to multiple # MythVideo entires for *.VOB files. Jamu does not process multi-part videos. # Added the use of PID files to prevent two instances of the same Jamu -M options running at the same # time. This deals with issues when a meta data source is off line for an extended # period of time and Jamu runs as a cronjob. Options effected are (-M, -MW and -MG). # Change to have jamu use the TMDB Movie title as is done in MythVideo rather than the file name. # Fixed a bug when TMDB genres are filtered and none remain they were still being added. This bug was # spotted and correct by Mathieu Brabant (thanks). # Added the ability for a user to filter additional characters from a file name this is important for # users using MS-Windows file systems as a CIFS mount. # 0.6.1 Added directory name parsing support for TV series title, series number and episode numbers. Patch # contributed by Mitko Haralanov (thanks). # 0.6.2 Added updating the 'homepage', 'releasedata' and 'hash' fields in the videometadata table # is the field exists. These fields is only present in trunk. # Properly initalize the homepage, hash, releasedate fields when adding a new videometadata record. # 0.6.3 Convert to new python bindings and replace all direct mysql data base calls. See ticket #7264 # Remove the requirement for the MySQLdb python library. # Removed the folder icon symlink code as it is redundant with MythVideo internal functionality. # The 'folderart' option is no longer support on a jamu.conf file and will be ignored if present. # Fixed a bug where a FE video directory was set but there were no FE image directories. # If there were local SG images directories set they were being used. This is an invalid # configuration that should have caused an error. # 0.6.4 Added a new option (-R) to allow just interactively populate the video reference numbers from # TVDB and TMDB without any meta data downloads. After that the user runs Jamu with the -M option # and the meta data and images are downloaded. # Added to the interactive interface the ability to select a reference number of '99999999' # which effectively tells jamu to ignore the specific video from all further processing. # Changed the return code from 1 to 0 when Jamu exits without processing if there is already an # instance of Jamu running. This was causing issues for Mythbuntu when TVDB or TMDB was down for an # extended period. # Added a new jamu.conf section [ignore-directory] to list Video sub-directories that Jamu should # ignore. # Change Jamu's import of tvdb and tmdb api libraries to use the installed versions found with the # MythTV python bindings. # Changed Jamu to use the tmdb API v2.0 python library # Jamu will always use the TMDB reference number over the IMDB number but still supports IMDB#s # Jamu interactive sessions for movies now lists the TMDB#s instead of IMDB#s # Jamu will convert any IMDB#s to TMDB#s when themoviedb.org includes that movie. This is in line # with MythVideo changes. Graphics for the movie will also be renamed so they do not need to be # re-downloaded. # Add the production countries for movies when TMDB provides that information. # Adjusted the -MW option to add a " Season 1" to any downloaded image filename for TV shows. # This must be done to make sure that TV shows like "24" do not clash with a movie's TMDB# like # (Kill Bill Vol.1) which is "24". # Added message display for exceptions where the message may enhance problem analysis. # Removed logic which checked that a TV episode was using Season graphics rather than Series graphics. # Unfortunately there was a chance that the a Series's graphics could clobber a movie with the same TMDB# # as the series title (e.g. the movie Kill Bill Vol.1 and the TV series 24). A positive is that a number # of redundant TV Series images can be removed through the jamu -MJ option. # Improved the -MW options detection of TV series when the EPG data does not include a subtitle. Users # can add the specific TVDB numbers to the 'series_name_override' section of the jamu.conf file. # Australian users had mentioned this as an issue, previously the TV series was always being mistaken # for a movie. # Jamu will now download the top rated TV Series season coverart and banner images. This enhancement # matches MythVideo processing. # 0.6.5 Small fix related to the bindings changes. # 0.6.6 Fixed Exception messages # Change all occurances of 'mythbeconn.host' to 'mythbeconn.hostname' to be consistent with bindings # 0.6.7 Fixed the (-J) janitor option from removing the Mirobridge default images when they are not being used # 0.6.8 Fixed a (-J) janitor option statistics error due to skipping Mirbridge default images # 0.6.9 Fixed an abort when IMDBpy returns movie matches with incomplete data # Fixed an abort where an IMDB# was being used instead of a TMDB# # Fixed an abort when a storage directory name caused an UnicodeEncodeError or TypeError exception # 0.7.0 Fixed an (-MW) option abort when a recorded program or upcoming program did not have a title # 0.7.1 Fixed a bug where movies with punctutation ("Mr. Magoo") were not finding matches # Fixed bug with interactive mode when a user enters a reference number directly rather than # making a list selection # These bugs were both identified by Edi Iten (thanks) # 0.7.2 Fixed a bug where an inetref field was not properly initialized and caused an abort. Ticket #8243 # 0.7.3 Fixed a bug where a user selected TMDB# was not being used. # Minor change to fuzzy matching of a file named parsed title with those from TMDB and TVDB. usage_txt=u''' JAMU - Just.Another.Metadata.Utility is a versatile utility for downloading graphics and meta data for both movies and TV Series information from themoviedb.com wiki and thetvdb.com wiki. In addition the MythTV data base is updated with the downloaded information. Here are the main uses for this utility: MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details. 1) Simple command line invocation to display or download data from thetvdb.com. Data can be one or more of: Posters/Cover art, Banners, Fan art, Episode Images and Episode meta data. use the command "jamu -e | less" to see command line examples. 2) Mass downloads of data matching your video files. ** This typically done once to download the information for your video collection. 3) Automated maintenance of the information in your video collection. ** 4) The creation of video file names which can be used to set the file name of your recorded TV shows. File names can be formated to the users preference with information like series name, season number, episode number and episode name. MythTV users may find this valuable as part of a user job that is spawned automatically by mythbackend when recording is finished. 5) Jamu's modules can be imported into your own python scripts to create enhanced functionality. 6) With the installation of free ImageMagick's utilities (specifically 'mogrify') you can resize graphics when they are downloaded. 7) Update the MythTV data base with links to posters, banners, fanart and episode images and optionally download missing graphics if they exist. This feature can be used for mass updates and regular maintenance. ''' examples_txt=u''' MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details. These examples are primarily for non-MythTV users of Jamu. jamu command line examples: NOTE: Included here are simple examples of jamu in action. Please review jamu_README for advise on how to get the most out of jamu. ( Display a TV series top rated poster fanart and banner URLs) > jamu -tS PBF "Sanctuary" poster:http://www.thetvdb.com/banners/posters/80159-1.jpg fanart:http://www.thetvdb.com/banners/fanart/original/80159-2.jpg banner:http://www.thetvdb.com/banners/graphical/80159-g2.jpg ( Display the URL for a TV series episode ) > jamu -tS I "Fringe" 1 5 filename:http://www.thetvdb.com/banners/episodes/82066-391049.jpg ( Display poster, fanart and banner graphics for a TV series but limited to two per type in a season ) > jamu -S PBF -m 2 "24" 4 poster:http://www.thetvdb.com/banners/seasons/76290-4-3.jpg poster:http://www.thetvdb.com/banners/seasons/76290-4.jpg fanart:http://www.thetvdb.com/banners/fanart/original/76290-1.jpg fanart:http://www.thetvdb.com/banners/fanart/original/76290-2.jpg banner:http://www.thetvdb.com/banners/seasonswide/76290-4.jpg banner:http://www.thetvdb.com/banners/seasonswide/76290-4-3.jpg ( Display a file name string (less file extention and directory path) for a TV episode ) > jamu -F "24" 4 3 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M. > jamu -F "24" "Day 4: 9:00 A.M.-10:00 A.M." 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M. ( Using SID number instead of series name ) > jamu -F 76290 4 3 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M. ( Simulate a dry run for the download of a TV series top rated poster and fanart ) > jamu -sdtS PF "Fringe" Simulation download of URL(http://www.thetvdb.com/banners/posters/82066-6.jpg) to File(~/Pictures/Poster - 82066-6.jpg) Get_Poster downloading successfully processed Simulation download of URL(http://www.thetvdb.com/banners/fanart/original/82066-11.jpg) to File(~/Pictures/Fanart - 82066-11.jpg) Get_Fanart downloading successfully processed ( Download the Episode meta data and episode image for a video file whose file name contains the series and season/episode numbers) > jamu -dS EI "~/Pictures/Fringe - S01E01.mkv" Episode meta data and/or images downloads successfully processed > ls -ls total 2 60 -rw-r--r-- 1 user user 53567 2009-03-12 22:05 Fringe - S01E01 - Pilot.jpg 4 -rw-r--r-- 1 user user 1059 2009-03-12 22:05 Fringe - S01E01 - Pilot.meta 4 -rw-r--r-- 1 user user 811 2009-03-12 13:22 Fringe - S01E01.mkv ( Display Episode meta data for a TV series ) > jamu -S E "24" 5 3 series:24 seasonnumber:5 episodenumber:3 episodename:Day 5: 9:00 A.M.-10:00 A.M. rating:None overview:Jack conceals himself inside the airport hanger and surveys the Russian separatists, feeding information to Curtis and his assault team. The terrorists begin executing hostages in an attempt to make Logan cave into their demands. Martha discovers that all traces of her conversation with Palmer may not have been erased. director:Brad Turner writer:Manny Coto gueststars:John Gleeson Connolly, V.J. Foster, David Dayan Fisher, Taylor Nichols, Steve Edwards, Taras Los, Joey Munguia, Reggie Jordan, Lou Richards, Karla Zamudio imdb_id:None filename:http://www.thetvdb.com/banners/episodes/76290-306117.jpg epimgflag:None language:en firstaired:2006-01-16 lastupdated:1197942225 productioncode:5AFF03 id:306117 seriesid:76290 seasonid:10067 absolute_number:None combined_season:5 combined_episodenumber:4.0 dvd_season:5 dvd_discid:None dvd_chapter:None dvd_episodenumber:4.0 ( Specify a user defined configuration file to set most of the configuration variables ) > jamu -C "~/.jamu/jamu.conf" -S P "Supernatural" poster:http://www.thetvdb.com/banners/posters/78901-3.jpg poster:http://www.thetvdb.com/banners/posters/78901-1.jpg ( Display in alphabetical order the state of all configuration variables ) > jamu -f allgraphicsdir (~/Pictures) bannerdir (None) config_file (False) data_flags (None) debug_enabled (False) download (False) ... lots of configuration variables ... video_dir (None) video_file_exts (['3gp', 'asf', 'asx', 'avi', 'mkv', 'mov', 'mp4', 'mpg', 'qt', 'rm', 'swf', 'wmv', 'm2ts', 'evo', 'ts', 'img', 'iso']) with_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s) without_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s) ''' # System modules import sys, os, re, locale, subprocess, locale, ConfigParser, urllib, codecs, shutil, datetime, fnmatch, string from datetime import date from optparse import OptionParser from socket import gethostname, gethostbyname import tempfile, struct import logging class OutStreamEncoder(object): """Wraps a stream with an encoder""" def __init__(self, outstream, encoding=None): self.out = outstream if not encoding: self.encoding = sys.getfilesystemencoding() else: self.encoding = encoding def write(self, obj): """Wraps the output stream, encoding Unicode strings with the specified encoding""" if isinstance(obj, unicode): try: self.out.write(obj.encode(self.encoding)) except IOError: pass else: try: self.out.write(obj) except IOError: pass def __getattr__(self, attr): """Delegate everything but write to the stream""" return getattr(self.out, attr) sys.stdout = OutStreamEncoder(sys.stdout, 'utf8') sys.stderr = OutStreamEncoder(sys.stderr, 'utf8') try: import xml except Exception, e: print '''The python module xml must be installed. error(%s)''' % e sys.exit(1) if xml.__version__ < u'41660': print ''' \n! Warning - The module xml (v41660 or greater) must be installed. Your version is different (v%s) than what Jamu was tested with. Jamu may not work on your installation.\nIt is recommended that you upgrade.\n''' % xml.__version__ import xml.etree.cElementTree as ElementTree # Find out if the MythTV python bindings can be accessed and instances can be created try: '''If the MythTV python interface is found, we can insert data directly to MythDB or get the directories to store poster, fanart, banner and episode graphics. ''' from MythTV import MythDB, DBData, Video, MythVideo, MythBE, FileOps, MythError, MythLog mythdb = None mythvideo = None mythbeconn = None try: '''Create an instance of each: MythDB, MythVideo ''' MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout mythdb = MythDB() mythvideo = MythVideo(mythdb) MythLog._setlevel('important,general') except MythError, e: print u'\n! Warning - %s' % e.args[0] filename = os.path.expanduser("~")+'/.mythtv/config.xml' if not os.path.isfile(filename): print u'\n! Warning - A correctly configured (%s) file must exist\n' % filename else: print u'\n! Warning - Check that (%s) is correctly configured\n' % filename except Exception, e: print u"\n! Warning - Creating an instance caused an error for one of: MythDBConn or MythVideo, error(%s)\n" % e try: localhostname = mythdb.gethostname() except: localhostname = gethostname() try: MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout mythbeconn = MythBE(backend=localhostname, db=mythdb) MythLog._setlevel('important,general') except MythError, e: print u'\nWith any -M option Jamu must be run on a MythTV backend' print u'! Warning - %s' % e.args[0] mythbeconn = None except Exception, e: print u"\n! Warning - MythTV python bindings could not be imported, error(%s)\n" % e mythdb = None mythvideo = None mythbeconn = None # Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available try: # thetvdb.com specific modules import MythTV.ttvdb.tvdb_ui as tvdb_ui # from tvdb_api import Tvdb import MythTV.ttvdb.tvdb_api as tvdb_api from MythTV.ttvdb.tvdb_exceptions import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_episodenotfound, tvdb_attributenotfound, tvdb_userabort) # verify version of tvdbapi to make sure it is at least 1.0 if tvdb_api.__version__ < '1.0': print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api.__version__ raise except Exception, e: print ''' The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py. They should have been installed along with the MythTV python bindings. Error(%s) ''' % e sys.exit(1) try: import MythTV.tmdb.tmdb_api as tmdb_api from MythTV.tmdb.tmdb_exceptions import (TmdBaseError, TmdHttpError, TmdXmlError, TmdbUiAbort, TmdbMovieOrPersonNotFound,) except Exception, e: sys.stderr.write(''' The subdirectory "tmdb" containing the modules tmdb_api.py (v0.1.3 or greater), tmdb_ui.py, tmdb_exceptions.py must have been installed with the MythTV python bindings. Error:(%s) ''' % e) sys.exit(1) if tmdb_api.__version__ < '0.1.3': sys.stderr.write("\n! Error: Your current installed tmdb_api.py version is (%s)\nYou must at least have version (0.1.3) or higher.\n" % tmdb_api.__version__) sys.exit(1) imdb_lib = True try: # Check if the installation is equiped to directly search IMDB for movies import imdb except ImportError, e: sys.stderr.write("\n! Error: To search for movies movies the IMDbPy library must be installed."\ "Check your installation's repository or check the following link."\ "from (http://imdbpy.sourceforge.net/?page=download)\nError:(%s)\n" % e) sys.exit(1) if imdb_lib: if imdb.__version__ < "3.8": sys.stderr.write("\n! Error: You version the IMDbPy library (%s) is too old. You must use version 3.8 of higher." % imdb.__version__) sys.stderr.write("Check your installation's repository or check the following link."\ "from (http://imdbpy.sourceforge.net/?page=download)\n") sys.exit(1) class VideoTypes( DBData ): table = 'videotypes' where = 'intid=%s' setwheredat = 'self.intid,' logmodule = 'Python VideoType' @staticmethod def getAll(db=None): db = MythDB(db) c = db.cursor() c.execute("""SELECT * FROM videotypes""") types = [] for row in c.fetchall(): types.append(VideoTypes(db=db, raw=row)) c.close() return types def __str__(self): return "" % self.extension def __repr__(self): return str(self).encode('utf-8') def __init__(self, id=None, ext=None, db=None, raw=None): if raw is not None: DBData.__init__(self, db=db, raw=raw) elif id is not None: DBData.__init__(self, data=(id,), db=db) elif ext is not None: self.__dict__['where'] = 'extension=%s' self.__dict__['wheredat'] = 'self.extension,' DBData.__init__(self, data=(ext,), db=db) # end VideoTypes() def isValidPosixFilename(name, NAME_MAX=255): """Checks for a valid POSIX filename Filename: a name consisting of 1 to {NAME_MAX} bytes used to name a file. The characters composing the name may be selected from the set of all character values excluding the slash character and the null byte. The filenames dot and dot-dot have special meaning. A filename is sometimes referred to as a "pathname component". name: (base)name of the file NAME_MAX: is defined in limits.h (implementation-defined constants) Maximum number of bytes in a filename (not including terminating null). Minimum Acceptable Value: {_POSIX_NAME_MAX} _POSIX_NAME_MAX: Maximum number of bytes in a filename (not including terminating null). Value: 14 More information on http://www.opengroup.org/onlinepubs/009695399/toc.htm """ return 1<=len(name)<= NAME_MAX and "/" not in name and "\000" not in name # end isValidPosixFilename() # Two routines used for movie title search and matching def is_punct_char(char): '''check if char is punctuation char return True if char is punctuation return False if char is not punctuation ''' return char in string.punctuation def is_not_punct_char(char): '''check if char is not punctuation char return True if char is not punctuation return False if chaar is punctuation ''' return not is_punct_char(char) def _getExtention(URL): """Get the graphic file extension from a URL return the file extention from the URL """ (dirName, fileName) = os.path.split(URL) (fileBaseName, fileExtension)=os.path.splitext(fileName) return fileExtension[1:] # end getExtention def _getFileList(dst): ''' Create an array of fully qualified file names return an array of file names ''' file_list = [] names = [] try: for directory in dst: try: directory = unicode(directory, 'utf8') except (UnicodeEncodeError, TypeError): pass for filename in os.listdir(directory): names.append(os.path.join(directory, filename)) except OSError, e: sys.stderr.write(u"\n! Error: Getting a list of files for directory (%s)\nThis is most likely a 'Permission denied' error\nError:(%s)\n\n" % (dst, e)) return file_list for video_file in names: if os.path.isdir(video_file): new_files = _getFileList([video_file]) for new_file in new_files: file_list.append(new_file) else: file_list.append(video_file) return file_list # end _getFileList class singleinstance(object): ''' singleinstance - based on Windows version by Dragan Jovelic this is a Linux version that accomplishes the same task: make sure that only a single instance of an application is running. ''' def __init__(self, pidPath): ''' pidPath - full path/filename where pid for running application is to be stored. Often this is ./var/.pid ''' from os import kill self.pidPath=pidPath # # See if pidFile exists # if os.path.exists(pidPath): # # Make sure it is not a "stale" pidFile # try: pid=int(open(pidPath, 'r').read().strip()) # # Check list of running pids, if not running it is stale so # overwrite # try: kill(pid, 0) pidRunning = 1 except OSError: pidRunning = 0 if pidRunning: self.lasterror=True else: self.lasterror=False except: self.lasterror=False else: self.lasterror=False if not self.lasterror: # # Write my pid into pidFile to keep multiple copies of program from # running. # fp=open(pidPath, 'w') fp.write(str(os.getpid())) fp.close() def alreadyrunning(self): return self.lasterror def __del__(self): if not self.lasterror: import os os.unlink(self.pidPath) # end singleinstance() # Global variables graphicsDirectories = {'banner': u'bannerdir', 'screenshot': u'episodeimagedir', 'coverfile': u'posterdir', 'fanart': u'fanartdir'} dir_dict={'posterdir': "VideoArtworkDir", 'bannerdir': 'mythvideo.bannerDir', 'fanartdir': 'mythvideo.fanartDir', 'episodeimagedir': 'mythvideo.screenshotDir', 'mythvideo': 'VideoStartupDir'} storagegroupnames = {u'Videos': u'mythvideo', u'Coverart': u'posterdir', u'Banners': u'bannerdir', u'Fanart': u'fanartdir', u'Screenshots': u'episodeimagedir'} storagegroups={u'mythvideo': [], u'posterdir': [], u'bannerdir': [], u'fanartdir': [], u'episodeimagedir': []} # The gobal dictionary is only populated with the current hosts storage group entries image_extensions = ["png", "jpg", "bmp"] def getStorageGroups(): '''Populate the storage group dictionary with the local host's storage groups. return nothing ''' records = mythdb.getStorageGroup(hostname=localhostname) if records: for record in records: # Only include Video, coverfile, banner, fanart, screenshot and trailers storage groups if record.groupname in storagegroupnames.keys(): dirname = record.dirname try: dirname = unicode(record.dirname, 'utf8') except (UnicodeDecodeError): sys.stderr.write(u"\n! Error: The local Storage group (%s) directory contained\ncharacters that caused a UnicodeDecodeError. This storage group has been rejected.'\n" % (record['groupname'])) continue # Skip any line that has non-utf8 characters in it except (UnicodeEncodeError, TypeError): pass # Strip the trailing slash so it is consistent with all other directory paths in Jamu if dirname[-1:] == u'/': storagegroups[storagegroupnames[record.groupname]].append(dirname[:-1]) else: storagegroups[storagegroupnames[record.groupname]].append(dirname) continue any_storage_group = False tmp_storagegroups = dict(storagegroups) for key in tmp_storagegroups.keys(): if len(tmp_storagegroups[key]): any_storage_group = True else: del storagegroups[key] # Remove empty SG directory arrays if any_storage_group: # Verify that each storage group is an existing local directory storagegroup_ok = True for key in storagegroups.keys(): for directory in storagegroups[key]: if not os.access(directory, os.F_OK): sys.stderr.write(u"\n! Error: The local Storage group (%s) directory (%s) does not exist\n" % (key, directory)) storagegroup_ok = False if not storagegroup_ok: sys.exit(1) # end getStorageGroups def _can_int(x): """Takes a string, checks if it is numeric. >>> _can_int("2") True >>> _can_int("A test") False """ if x == None: return False try: int(x) except ValueError: return False else: return True # end _can_int class BaseUI: """Default non-interactive UI, which auto-selects first results """ def __init__(self, config, log): self.config = config self.log = log def selectSeries(self, allSeries): return allSeries[0] def selectMovieOrPerson(self, allElements): return makeDict([allElements[0]]) # Local variable video_type = u'' UI_title = u'' UI_search_language = u'' UI_selectedtitle = u'' # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml UI_langid_dict = {u'da': u'10', 'fi': u'11', u'nl': u'13', u'de': u'14', u'it': u'15', u'es': u'16', u'fr': u'17', u'pl': u'18', u'hu': u'19', u'el': u'20', u'tr': u'21', u'ru': u'22', u'he': u'24', u'ja': u'25', u'pt': u'26', u'zh': u'27', u'cs': u'28', u'sl': u'30', u'hr': u'31', u'ko': u'32', u'en': '7', u'sv': u'8', u'no': u'9',} class jamu_ConsoleUI(BaseUI): """Interactively allows the user to select a show or movie from a console based UI """ def _displaySeries(self, allSeries_array): """Helper function, lists series with corresponding ID """ if video_type == u'IMDB': URL = u'http://www.imdb.com/title/tt' URL2 = u'http://www.imdb.com/find?s=all&q='+urllib.quote_plus(UI_title.encode("utf-8"))+'&x=0&y=0' reftype = u'IMDB' elif video_type == u'TMDB': URL = u'http://themoviedb.org/movie/' URL2 = u'http://themoviedb.org/' reftype = u'TMDB' else: # TVDB URL = u'http://thetvdb.com/index.php?tab=series&id=%s&lid=%s' URL2 = u'http://thetvdb.com/?tab=advancedsearch' reftype = u'thetvdb' tmp_title = u'' allSeries={} for index in range(len(allSeries_array)): allSeries[allSeries_array[index]['name']] = allSeries_array[index] tmp_names = allSeries.keys() tmp_names.sort() most_likely = [] # Find any TV Shows or Movies who's titles start with the video's title for name in tmp_names: if filter(is_not_punct_char, name.lower()).startswith(filter(is_not_punct_char, UI_title.lower())): most_likely.append(name) # IMDB can return titles that are a movies foriegn title. The titles that do not match # the requested title need to be added to the end of the most likely titles list. if video_type == u'IMDB' and len(most_likely): for name in tmp_names: try: dummy = most_likely.index(name) except ValueError: most_likely.append(name) names = [] # Remove any name that does not start with a title like the TV Show/Movie (except for IMDB) if len(most_likely): for likely in most_likely: names.append(likely) else: names = tmp_names if not video_type == u'IMDB': names.sort() # reorder the list of series and sid's new_array=[] for key in names: # list all search results new_array.append(allSeries[key]) # If there is only one to select and it is an exact match then return with no interface display if len(new_array) == 1: if filter(is_not_punct_char, allSeries_array[0]['name'].lower()) == filter(is_not_punct_char, UI_title.lower()): return new_array # Add the ability to select the skip inetref of '99999999' new_array.append( {'sid': '99999999', 'name': u'User choses to ignore video'} ) names.append(u'User choses to ignore video') i_show=0 for key in names: # list all search results i_show+=1 # Start at more human readable number 1 (not 0) if key == u'User choses to ignore video': print u"% 2s -> %s # %s" % ( i_show, '99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'" ) continue if video_type != u'IMDB' and video_type != u'TMDB': tmp_URL = URL % (allSeries[key]['sid'], UI_langid_dict[UI_search_language]) print u"% 2s -> %s # %s" % ( i_show, key, tmp_URL ) else: print u"% 2s -> %s # %s%s" % ( i_show, key, URL, allSeries[key]['sid'] ) print u"Direct search of %s # %s" % ( reftype, URL2 ) return new_array def selectSeries(self, allSeries): global UI_selectedtitle UI_selectedtitle = u'' allSeries = self._displaySeries(allSeries) # Check for an automatic choice if len(allSeries) <= 2: for series in allSeries: if filter(is_not_punct_char, series['name'].lower()) == filter(is_not_punct_char, UI_title.lower()): UI_selectedtitle = series['name'] return series display_total = len(allSeries) if video_type == u'IMDB': reftype = u'IMDB #' refsize = 7 refformat = u"%07d" elif video_type == u'TMDB': reftype = u'TMDB #' refsize = 5 refformat = u"%05d" else: reftype = u'Series id' refsize = 5 refformat = u"%6d" # Attempt to have the most likely TV/Movies at the top of the list while True: # return breaks this loop try: print u'Enter choice ("Enter" key equals first selection (1)) or input the %s directly, ? for help):' % reftype ans = raw_input() except KeyboardInterrupt: raise tvdb_userabort(u"User aborted (^c keyboard interupt)") except EOFError: raise tvdb_userabort(u"User aborted (EOF received)") self.log.debug(u'Got choice of: %s' % (ans)) try: if ans == '': selected_id = 0 else: selected_id = int(ans) - 1 # The human entered 1 as first result, not zero except ValueError: # Input was not number if ans == u"q": self.log.debug(u'Got quit command (q)') raise tvdb_userabort(u"User aborted ('q' quit command)") elif ans == u"?": print u"## Help" print u"# Enter the number that corresponds to the correct video." print u"# Enter the %s number for the %s." % (reftype, video_type) print u"# ? - this help" print u"# q - abort" else: self.log.debug(u'Unknown keypress %s' % (ans)) else: self.log.debug(u'Trying to return ID: %d' % (selected_id)) try: UI_selectedtitle = allSeries[selected_id]['name'] return allSeries[selected_id] except IndexError: if len(ans) == refsize and reftype != u'Series id': UI_selectedtitle = u'' return {'name': u'User input', 'sid': ans} elif reftype == u'Series id': if len(ans) >= refsize: UI_selectedtitle = u'' return {'name': u'User input', 'sid': ans} self.log.debug(u'Invalid number entered!') print u'Invalid number (%d) input! A directly entered %s must be a full %d zero padded digits (e.g. 905 should be entered as %s)' % (selected_id, reftype, refsize, refformat % 905) UI_selectedtitle = u'' self._displaySeries(allSeries) #end try #end while not valid_input def _useImageMagick(cmd): """ Process graphics files using ImageMagick's utility 'mogrify'. >>> _useImageMagick('-resize 50% "poster.jpg"') >>> 0 >>> -1 """ return subprocess.call(u'mogrify %s > /dev/null' % cmd, shell=True) # end verifyImageMagick # Call a execute a command line process def callCommandLine(command): '''Call a command line script or program. Display any errors return all stdoutput as a string ''' p = subprocess.Popen(command, shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) while 1: data = p.stderr.readline() if len(data): sys.stderr.write(u'%s\n' % data) if data == '' and p.poll() != None: break returned_data='' while 1: data = p.stdout.readline() if len(data): returned_data+=data if data == '' and p.poll() != None: break return returned_data # end callCommandLine # All functionality associated with configuration options class Configuration(object): """Set defaults, apply user configuration options, validate configuration settings and display the settings. To view all available options run: >>> config = Configuration() >>> config.displayOptions() """ def __init__(self, interactive = False, debug = False): """Initialize default configuration settings """ self.config = {} # Set all default variables self.config['interactive'] = interactive self.config['debug_enabled'] = debug self.config['flags_options'] = False self.config['local_language'] = u'en' self.config['simulation'] = False self.config['toprated'] = False self.config['download'] = False self.config['nokeys'] = False self.config['maximum'] = None self.config['user_config'] = None self.config['overwrite'] = False self.config['update'] = False self.config['mythtvdir'] = False self.config['hd_dvd'] = ' HD - On DVD' # Used for HD DVD collection zero length video files self.config['dvd'] = ' - On DVD' # Used for DVD collection zero length video files self.config['video_dir'] = None self.config['recursive'] = True self.config['series_name'] = None self.config['sid'] = None self.config['season_num'] = None self.config['episode_num'] = None self.config['episode_name'] = None self.config['ret_filename'] = False # Flags for which data to perform actions on self.config['get_poster'] = False self.config['get_banner'] = False self.config['get_fanart'] = False self.config['get_ep_image'] = False self.config['get_ep_meta'] = False self.config['data_flags'] = '' self.config['tmdb_genre_filter'] = ['action film', 'adventure film', 'comedy', 'crime film', 'disaster film', 'documentary film', 'drama film', 'eastern', 'environmental', 'fantasy film', 'historical film', 'horror film', 'musical film', 'mystery', 'mystery film', 'road movie', 'science fiction film', 'sport', 'thriller', 'western', 'film noir', 'cult movie', 'neo-noir', 'guy movie',] self.config['log'] = self._initLogger() # Setups the logger (self.log.debug() etc) # The default format of the file names (with and without episode names) self.config['with_ep_name'] = u'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s' self.config['without_ep_name'] = u'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s' self.config['ep_metadata'] = self.config['with_ep_name'] # The default format of the graphics file names (with and without seasons and/or episode names) # The default is to use the URL's filename from thetvdb.com self.config['g_defaultname']=True # e.g. "Fringe - 01.jpg" self.config['g_series'] = u'%(series)s - %(seq)s.%(ext)s' # e.g. "SG-1 - 07-02.jpg" self.config['g_season'] = u'%(series)s - %(seasonnumber)02d-%(seq)s.%(ext)s' # Set default configuration variables # Start - Variables the user can override through option "-u" with their own file of variables self.config['allgraphicsdir'] = os.getcwd() self.config['posterdir'] = None self.config['bannerdir'] = None self.config['fanartdir'] = None self.config['episodeimagedir'] = None self.config['metadatadir'] = None self.config['mythtvmeta'] = False self.config['myth_secondary_sources'] = {} self.config['posterresize'] = False self.config['fanartresize'] = False self.config['min_poster_size'] = 400 self.config['image_library'] = False self.config['ffmpeg'] = True self.config['folderart'] = False self.config['metadata_exclude_as_update_trigger'] = ['intid', 'season', 'episode', 'showlevel', 'filename', 'coverfile', 'childid', 'browse', 'playcommand', 'trailer', 'host', 'screenshot', 'banner', 'fanart'] self.config['filename_char_filter'] = u"/%\000" self.config['ignore-directory'] = [] # Dictionaries for Miro Bridge metadata downlods self.config['mb_tv_channels'] = {} self.config['mb_movies'] = {} # Episode data keys that you want to display or download. # This includes the order that you want them display or in the downloaded file. self.config['ep_include_data'] = [u'series', u'seasonnumber', u'episodenumber', u'episodename', u'rating', u'overview', u'director', u'writer', u'cast', u'gueststars', u'imdb_id', u'filename', u'epimgflag', u'language', u'runtime', u'firstaired', u'genres', u'lastupdated', u'productioncode', u'id', u'seriesid', u'seasonid', u'absolute_number', u'combined_season', u'combined_episodenumber', u'dvd_season', u'dvd_discid', u'dvd_chapter', u'dvd_episodenumber'] self.config['config_file'] = False self.config['series_name_override'] = False self.config['ep_name_massage'] = False self.config['video_file_exts'] = [u'3gp', u'asf', u'asx', u'avi', u'mkv', u'mov', u'mp4', u'mpg', u'qt', u'rm', u'swf', u'wmv', u'm2ts', u'ts', u'evo', u'img', u'iso'] # Regex pattern strings used to check for season number from directory names self.config['season_dir_pattern'] = [ # foo_01 ???? re.compile(u'''^.+?[ \._\-]([0-9]+)[^\\/]*$''', re.UNICODE), # foo_S01 ???? re.compile(u'''^.+?[ \._\-][Ss]([0-9]+)[^\\/]*$''', re.UNICODE), # 01 ???? re.compile(u'''([0-9]+)[^\\/]*$''', re.UNICODE), # s01 ???? re.compile(u'''[Ss]([0-9]+)[^\\/]*$''', re.UNICODE), ] # Set default regex pattern strings used to extract series name , season and episode numbers for file name self.config['name_parse'] = [ # foo_[s01]_[e01] re.compile(u'''^(.+?)[ \._\-]\[[Ss]([0-9]+?)\]_\[[Ee]([0-9]+?)\]?[^\\/]*$''', re.UNICODE), # foo.1x09* re.compile(u'''^(.+?)[ \._\-]\[?([0-9]+)x([0-9]+)[^\\/]*$''', re.UNICODE), # foo.s01.e01, foo.s01_e01 re.compile(u'''^(.+?)[ \._\-][Ss]([0-9]+)[\.\- ]?[Ee]([0-9]+)[^\\/]*$''' , re.UNICODE), # foo.103* re.compile(u'''^(.+)[ \._\-]([0-9]{1})([0-9]{2})[\._ -][^\\/]*$''' , re.UNICODE), # foo.0103* re.compile(u'''^(.+)[ \._\-]([0-9]{2})([0-9]{2,3})[\._ -][^\\/]*$''' , re.UNICODE), ] # regex strings to parse folder names for TV series title, season and episode numbers self.config['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': u'Season', u'episode': u'Episode'} self.config['fullname_parse_regex'] = [ # Title/Season 1/s01e01 Subtitle u'''^.+?/(?P[^/]+)/%(season)s%(slash)s '''+ u'''(?P[0-9]+)/[Ss][0-9]+[Ee](?P[0-9]+).+$''', # ramsi # Title/Season 1/1x01 Subtitle u'''^.+?/(?P[^/]+)/%(season)s%(slash)s '''+ u'''[0-9]+/(?P[0-9]+)[Xx](?P[0-9]+).+$''', # Title/Season 1/01 Subtitle u'''^.+?/(?P[^/]+)/%(season)s%(slash)s '''+ u'''(?P[0-9]+)/(?P[0-9]+).+$''', # Title/Season 1/Title s01e01 Subtitle u'''^.+?/(?P[^/]+)/%(season)s%(slash)s '''+ u'''(?P[0-9]+)/(?:(?P=seriesname))%(slash)s [Ss][0-9]+'''+ u'''[Ee](?P[0-9]+).+$''', # Title/Season 1/Title 1x01 Subtitle u'''^.+?/(?P[^/]+)/%(season)s%(slash)s '''+ u'''(?P[0-9]+)/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))'''+ u'''[Xx](?P[0-9]+).+$''', # Title/Season 1/Episode 1 Subtitle u'''^.+?/(?P[^/]+)/%(season)s%(slash)s '''+ u'''(?P[0-9]+)/%(episode)s%(slash)s (?P[0-9]+).+$''', # Title/Season 1/Season 1 Episode 1 Subtitle u'''^.+?/(?P[^/]+)/%(season)s%(slash)s '''+ u'''(?P[0-9]+)/%(season)s%(slash)s (?:(?P=seasno))%(slash)s '''+ u'''%(episode)s%(slash)s (?P[0-9]+).+$''', # Title Season 1/01 Subtitle u'''^.+?/(?P[^/]+)%(slash)s %(season)s%(slash)s (?P[0-9]+)'''+ u'''/(?P[0-9]+).+$''', # Title Season 1/s01e01 Subtitle u'''^.*?/(?P[^/]+)%(slash)s %(season)s%(slash)s (?P[0-9]+)'''+ u'''/[Ss][0-9]+[Ee](?P[0-9]+).+''', # Title Season 1/1x01 Subtitle u'''^.*?/(?P[^/]+)%(slash)s %(season)s%(slash)s (?P[0-9]+)'''+ u'''/(?:(?P=seasno))[Xx](?P[0-9]+).+$''', # Title Season 1/Title s01e01 Subtitle u'''^.*?/(?P[^/]+)%(slash)s %(season)s%(slash)s (?P[0-9]+)'''+ u'''/(?:(?P=seriesname))%(slash)s [Ss][0-9]+[Ee](?P[0-9]+).+$''', # Title Season 1/Title 1x01 Subtitle u'''^.*?/(?P[^/]+)%(slash)s %(season)s%(slash)s (?P[0-9]+)'''+ u'''/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))[Xx](?P[0-9]+).+$''', # Title Season 1/Episode 1 Subtitle u'''^.*?/(?P[^/]+)%(slash)s %(season)s%(slash)s (?P[0-9]+)'''+ u'''/%(episode)s%(slash)s (?P[0-9]+).+$''', # Title Season 1/Season 1 Episode 1 Subtitle u'''^.*?/(?P[^/]+)%(slash)s %(season)s%(slash)s (?P[0-9]+)'''+ u'''/%(season)s%(slash)s (?:(?P=seasno))%(slash)s %(episode)s%(slash)s (?P[0-9]+).+$''' ] # Initalize a valriable used by the -MW option self.program_seriesid = None self.config[u'file_move_flag'] = False # end __init__ # Local variable data_flags_table={ 'P': 'get_poster', 'B': 'get_banner', 'F': 'get_fanart', 'I': 'get_ep_image', 'E': 'get_ep_meta'} def _initLogger(self): """Sets up a logger using the logging module, returns a log object """ logger = logging.getLogger(u"jamu") formatter = logging.Formatter(u'%(asctime)s) %(levelname)s %(message)s') hdlr = logging.StreamHandler(sys.stdout) hdlr.setFormatter(formatter) logger.addHandler(hdlr) if self.config['debug_enabled']: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.WARNING) return logger #end initLogger def setUseroptions(self, useroptions): """ Change variables through a user supplied configuration file return False and exit the script if there are issues with the configuration file values """ if useroptions[0]=='~': useroptions=os.path.expanduser("~")+useroptions[1:] if os.path.isfile(useroptions) == False: sys.stderr.write( "\n! Error: The specified user configuration file (%s) is not a file\n" % useroptions ) sys.exit(1) cfg = ConfigParser.SafeConfigParser() cfg.read(useroptions) for section in cfg.sections(): if section[:5] == 'File ': self.config['config_file'] = section[5:] continue if section == 'variables': # Change variables per user config file for option in cfg.options(section): if option == 'video_file_exts' or option == 'tmdb_genre_filter' or option == 'metadata_exclude_as_update_trigger': tmp_list = (cfg.get(section, option).rstrip()).split(',') for i in range(len(tmp_list)): tmp_list[i] = (tmp_list[i].strip()).lower() self.config[option] = tmp_list continue if option == 'filename_char_filter': for char in cfg.get(section, option): self.config['filename_char_filter']+=char continue if option == 'translate': s_e = (cfg.get(section, option).rstrip()).split(',') if not len(s_e) == 2: continue for index in range(len(s_e)): s_e[index] = s_e[index].strip() self.config['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': s_e[0], u'episode': s_e[1]} continue # Ignore user settings for Myth Video and graphics file directories # when the MythTV metadata option (-M) is selected if self.config['mythtvmeta'] and option in ['posterdir', 'bannerdir', 'fanartdir', 'episodeimagedir', 'mythvideo']: continue self.config[option] = cfg.get(section, option) continue if section == 'regex': # Change variables per user config file for option in cfg.options(section): self.config['name_parse'].append(re.compile(unicode(cfg.get(section, option), 'utf8'), re.UNICODE)) continue if section == 'ignore-directory': # Video directories to be excluded from Jamu processing for option in cfg.options(section): self.config['ignore-directory'].append(unicode(cfg.get(section, option), 'utf8')) continue if section =='series_name_override': overrides = {} for option in cfg.options(section): overrides[option] = cfg.get(section, option) if len(overrides) > 0: self.config['series_name_override'] = overrides continue if section =='ep_name_massage': massage = {} for option in cfg.options(section): tmp =cfg.get(section, option).split(',') if len(tmp)%2 and len(cfg.get(section, option)) != 0: sys.stderr.write(u"\n! Error: For (%s) 'ep_name_massage' values must be in pairs\n" % option) sys.exit(1) tmp_array=[] i=0 while i != len(tmp): tmp[i] = tmp[i].strip() tmp[i+1] = tmp[i+1].strip() tmp_array.append([tmp[i].replace('"',''), tmp[i+1].replace('"','')]) i+=2 massage[option]=tmp_array if len(massage) > 0: self.config['ep_name_massage'] = massage continue if section == 'ep_metadata_to_download': if len(cfg.options(section)): if cfg.options(section)[0] == 'ep_include_data': tmp=cfg.get(section, cfg.options(section)[0]) overrides=tmp.split(',') for index in range(len(overrides)): x = overrides[index].replace(' ','') if len(x) != 0: overrides[index]=x else: del overrides[index] self.config['ep_include_data']=overrides continue if section == 'data_flags': if len(cfg.options(section)): for option in cfg.options(section): if cfg.get(section, option).lower() != 'False'.lower(): for key in self.data_flags_table.keys(): if option == self.data_flags_table[key]: self.config[option] = True continue for sec in ['movies-secondary-sources', 'tv-secondary-sources']: if section == sec: secondary = {} for option in cfg.options(section): secondary[option] = cfg.get(section, option) if len(secondary) > 0: self.config['myth_secondary_sources'][sec[:sec.index('-')]] = secondary continue if section == u'mb_tv': # Add the channel names and their corresponding thetvdb.com id numbers for option in cfg.options(section): self.config['mb_tv_channels'][filter(is_not_punct_char, option.lower())] = [cfg.get(section, option), u''] continue if section == u'mb_movies': # Add the channel names for movie trailer Channels for option in cfg.options(section): self.config['mb_movies'][filter(is_not_punct_char, option.lower())] = cfg.get(section, option) continue # Expand any home directories that are not fully qualified dirs_to_check= [u'bannerdir', u'episodeimagedir', u'metadatadir', u'posterdir', u'video_dir', u'fanartdir'] for item in dirs_to_check: if self.config[item]: if item == u'metadatadir' and not self.config[item]: continue if self.config[item][0]=='~': self.config[item]=os.path.expanduser("~")+self.config[item][1:] # end setUserconfig def displayOptions(self): """ Display all of the configuration values. This is used to verify that the user has the variables set as they want before running jamu live. """ keys=self.config.keys() keys.sort() ################### Used to create the example configuration file "jamu-example-conf" # for key in keys: # Used to create the example configuration file "jamu-example-conf" # print "#%s: %s" % (key, self.config[key]) # sys.exit(0) ################## for key in keys: if key == 'log': # Do not display the logger instance it is irrelevant for display continue try: if key == 'name_parse': print u"%s (%d items)" % (key, len(self.config[key])) else: print u"%s (%s)" % (key, str(self.config[key])) except: try: print u"%s (%d items)" % (key, len(self.config[key])) except: print u"%s:" % key, self.config[key] # end set_Userconfig def changeVariable(self, key, value): """Change any configuration variable - caution no validation is preformed """ self.config[key]=value # end changeVariable def _checkNFS(self, dirs, ext_filter): '''Check if any of the files are on NFS shares. If they are then the user must be warned. return True if there are at least one file is on a NFS share. return False if no graphic files are on an NFS share. ''' tmp_dirs = [] for d in dirs: # Get rid of Null directories if d: tmp_dirs.append(d) dirs = tmp_dirs global localhostname, graphicsDirectories try: localip = gethostbyname(localhostname) # Get the local hosts IP address except Exception, e: sys.stderr.write("\n! Error: There is no valid address-to-host mapping for the host (%s)\nThe Jamu Janitor (-MJ) option cannot be used while this issue remains un-resolved.\nError:(%s)\n" % (localhostname, e)) sys.exit(1) # Get all curently mounted NFS shares tmp_mounts = callCommandLine("mount -l | grep '//'").split('\n') nfs = [] for mount in tmp_mounts: mount.rstrip() parts = mount.split(' ') tmparray=[P for P in parts] if tmparray[0].startswith('//'): # Is this a NFS share definition if not tmparray[0].startswith(u'//%s' % localip) and not tmparray[0].startswith(u'//%s' % localhostname): nfs.append(tmparray[2]) # Add an NFS mount name if not len(nfs): # Check if there are any NFS mounts return False # Check if any of the directories have files on an NFS share for directory in dirs: # Check the base directories first for mount in nfs: if os.path.realpath(directory).startswith(mount): return True for directory in dirs: # Check the actual files file_list = _getFileList([directory]) if not len(file_list): continue tmp_list = [] for fle in file_list: # Make a copy of file_list tmp_list.append(fle) for g_file in tmp_list: # Cull the list removing dirs and non-extention files if os.path.isdir(g_file): file_list.remove(g_file) continue g_ext = _getExtention(g_file) if not g_ext.lower() in ext_filter: file_list.remove(g_file) continue for filename in file_list: # Actually check each file against the NFS mounts for mount in nfs: if os.path.realpath(filename).startswith(mount): return True return False # end _checkNFS def _getMythtvDirectories(self): """Get all graphics directories found in the MythTV DB and change their corresponding configuration values. /media/video:/media/virtual/VB_Share/Review """ # Stop processing if this local host has any storage groups global localhostname, storagegroups # Make sure Jamu is being run on a MythTV backend if not mythbeconn: sys.stderr.write(u"\n! Error: Jamu must be run on a MythTV backend. Local host (%s) is not a MythTV backend.\n" % localhostname) sys.exit(1) global dir_dict for key in dir_dict.keys(): graphics_dir = mythdb.settings[localhostname][dir_dict[key]] # Only use path from MythTV if one was found self.config[key] = [] if key == 'mythvideo' and graphics_dir: tmp_directories = graphics_dir.split(':') if len(tmp_directories): for i in range(len(tmp_directories)): tmp_directories[i] = tmp_directories[i].strip() if tmp_directories[i] != '': if os.access(tmp_directories[i], os.F_OK): self.config[key].append(tmp_directories[i]) continue else: sys.stderr.write(u"\n! Warning: MythTV video directory (%s) does not exist.\n" % (tmp_directories[i])) continue if key != 'mythvideo' and graphics_dir: if os.path.os.access(graphics_dir, os.F_OK): self.config[key] = [graphics_dir] else: sys.stderr.write(u"\n! Warning: MythTV (%s) directory (%s) does not exist.\n" % (key, graphics_dir)) # Save the FE path settings local to this backend self.config['localpaths'] = {} for key in dir_dict.keys(): self.config['localpaths'][key] = [] local_paths = [] if len(self.config[key]): self.config['localpaths'][key] = list(self.config[key]) # If there is a Videos SG then there is always a Graphics SG using Videos as a fallback getStorageGroups() for key in dir_dict.keys(): if key == 'episodeimagedir' or key == 'mythvideo': continue if storagegroups.has_key(u'mythvideo') and not storagegroups.has_key(key): storagegroups[key] = list(storagegroups[u'mythvideo']) # Set fall back # Use Storage Groups as the priority but append any FE directory settings that # are local to this BE but are not already used as a storage group if storagegroups.has_key(u'mythvideo'): for key in storagegroups.keys(): self.config[key] = list(storagegroups[key]) for k in self.config['localpaths'][key]: if not k in self.config[key]: self.config[key].append(k) # Add any FE settings local directories not already included else: if key == 'mythvideo': sys.stdout.write(u"\n! Warning: You have a front end video directory path that is a duplicate of this backend's 'Videos' storage group.\nFront end directory (%s)\nThe Front end setting has been ignored.\nThis Front end video directory will cause duplicate entires in MythVideo.\n" % (k)) else: sys.stdout.write(u"\n! Info: You have a front end directory path that is a duplicate of this backend's storage group.\nFront end directory (%s)\nThe Front end setting has been ignored.\n" % (k)) continue # Make sure there is a directory set for Videos and other graphics directories on this host exists = True for key in dir_dict.keys(): if key == 'episodeimagedir': # Jamu does nothing with Screenshots continue # The fall back graphics SG is the Videos SG directory as of changeset 22104 if storagegroups.has_key(u'mythvideo') and not len(self.config[key]): self.config[key] = storagegroups[u'mythvideo'] if not len(self.config[key]): sys.stderr.write(u"\n! Error: There must be a directory for Videos and each graphic type. The (%s) directory is missing.\n" % (key)) exists = False if not exists: sys.exit(1) # Make sure that the directory set for Videos and other graphics directories have the proper permissions accessable = True for key in dir_dict.keys(): for directory in self.config[key]: if key == 'episodeimagedir': # Jamu does nothing with Screenshots continue if key == 'mythvideo': if not os.access(directory, os.F_OK | os.R_OK): sys.stderr.write(u"\n! Error: This video directory must have read access for Jamu to function.\nThere is a permissions issue with (%s).\n" % (directory, )) accessable = False continue if not os.access(directory, os.F_OK | os.R_OK | os.W_OK): sys.stderr.write(u"\n! Error: The (%s) directory (%s) must be read/writable for Jamu to function.\n" % (key, directory, )) accessable = False if not accessable: sys.exit(1) # Print out the video and image directories that will be used for processing if self.config['mythtv_verbose']: dir_types={'posterdir': "Cover art ", 'bannerdir': 'Banners ', 'fanartdir': 'Fan art ', 'episodeimagedir': 'Screenshots', 'mythvideo': 'Video '} sys.stdout.write(u"\n==========================================================================================\n") sys.stdout.write(u"Listed below are the types and base directories Jamu will use for processing.\nThe list reflects your current configuration for the '%s' back end\nand whether a directory is a 'SG' (storage group) or not.\n" % localhostname) sys.stdout.write(u"Note: All directories are from settings in the MythDB specific to hostname (%s).\n" % localhostname) sys.stdout.write(u"Note: Screenshot directories are not listed as Jamu does not process Screenshots.\n") sys.stdout.write(u"------------------------------------------------------------------------------------------\n") for key in dir_dict.keys(): if key == 'episodeimagedir': continue for directory in self.config[key]: sg_flag = 'NO ' if storagegroups.has_key(key): if directory in storagegroups[key]: sg_flag = 'YES' sys.stdout.write(u"Type: %s - SG-%s - Directory: (%s)\n" % (dir_types[key], sg_flag, directory)) sys.stdout.write(u"------------------------------------------------------------------------------------------\n") sys.stdout.write(u"If a directory you set from a separate Front end is not displayed it means\nthat the directory is not accessible from this backend OR\nyou must add the missing directories using the Front end on this Back end.\nFront end settings are host machine specific.\n") sys.stdout.write(u"==========================================================================================\n\n") if self.config[u'file_move_flag']: # verify the destination directory in a move is read/writable index = 0 accessable = True for arg in self.args: if index % 2 == 0: index+=1 continue if not os.access(arg, os.F_OK): for dirct in self.config['mythvideo']: if arg.startswith(dirct): if not os.access(dirct, os.F_OK | os.R_OK | os.W_OK): sys.stderr.write(u"! Error: Your move destination root MythVideo directory (%s) must be read/writable for Jamu to function.\n\n" % (dirct, )) accessable = False break else: sys.stderr.write(u"! Error: Your move destination directory (%s) must be a MythVideo directory OR a subdirectory of a MythVideo directory.\n\n" % (arg, )) accessable = False elif not os.access(arg, os.F_OK | os.R_OK | os.W_OK): sys.stderr.write(u"! Error: Your move destination directory (%s) must be read/writable for Jamu to function.\n\n" % (arg, )) accessable = False index+=1 if not accessable: sys.exit(1) # Check if any Video files are on a NFS shares if not self.config['mythtvNFS']: # Maybe the NFS check is to be skipped if self._checkNFS(self.config['mythvideo'], self.config['video_file_exts']): sys.stderr.write(u"\n! Error: Your video files reside on a NFS mount.\nIn the case where you have more than one MythTV backend using the same directories to store either video files\nor graphics any Jamu's option (-M) can adversly effect your MythTV database by mistakenly adding videos\nfor other backends or with the Janitor (-J) option mistakenly remove graphics files.\n\nIf you only have one backend or do not mix the Video or graphic file directories between backends and still want to use\nJamu add the options (N) to your option string e.g. (-MJN), which will skip this check.\n\n") sys.exit(1) # end _getMythtvDirectories def _JanitorConflicts(self): '''Verify that there are no conflict between the graphics directories of MythVideo and other MythTV plugins. Write an warning message if a conflict is found. return True when there is a conflict return False when there is no conflict ''' # Except for the plugins below no other plugins have non-theme graphics # MythGallery: # Table 'settings' fields 'GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation' # MythGame: # Table 'settings' fields 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir' # MythMusic: # Table 'settings' fields 'MusicLocation' global graphicsDirectories, localhostname tablefields = ['GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation', 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir', 'MusicLocation', 'ScreenShotPath'] returnvalue = False # Initalize as no conflicts for field in tablefields: tmp_setting = mythdb.settings[localhostname][field] if not tmp_setting: continue settings = tmp_setting.split(':') # Account for multiple dirs per setting if not len(settings): continue for setting in settings: for directory in graphicsDirectories.keys(): if not self.config[graphicsDirectories[directory]]: continue # As the Janitor processes subdirectories matching must be a starts with check for direc in self.config[graphicsDirectories[directory]]: if os.path.realpath(setting).startswith(os.path.realpath(direc)): sys.stderr.write(u"\n! Error - The (%s) directory (%s) conflicts\nwith the MythVideo (%s) directory (%s).\nThe Jamu Janitor (-MJ) option cannot be used.\n\n" % (field, setting, direc, self.config[graphicsDirectories[directory]]) ) returnvalue = True return returnvalue # end _JanitorConflicts def _addMythtvUserFileTypes(self): """Add video file types to the jamu list from the "videotypes" table """ # Get videotypes table field names: try: records = VideoTypes.getAll() except MythError, e: sys.stderr.write(u"\n! Error: Reading videotypes MythTV table: %s\n" % e.args[0]) return False if records: for record in records: # Remove any extentions that are in Jamu's list but the user wants ignore if record.f_ignore: if record.extension in self.config['video_file_exts']: self.config['video_file_exts'].remove(record.extension) if record.extension.lower() in self.config['video_file_exts']: self.config['video_file_exts'].remove(record.extension.lower()) else: # Add extentions that are not in the Jamu list if not record.extension in self.config['video_file_exts']: self.config['video_file_exts'].append(record.extension) # Make sure that all video file extensions are lower case for index in range(len(self.config['video_file_exts'])): self.config['video_file_exts'][index] = self.config['video_file_exts'][index].lower() # end _addMythtvUserFileTypes() def validate_setVariables(self, args): """Validate the contents of specific configuration variables return False and exit the script if an invalid configuation value is found """ # Fix all variables which were changed by a users configuration files # to 'None', 'False' and 'True' literals back to their intended values keys=self.config.keys() types={'None': None, 'False': False, 'True': True} for key in keys: for literal in types.keys(): if self.config[key] == literal: self.config[key] = types[literal] # Compile regex strings to parse folder names for TV series title, season and episode numbers self.config['fullname_parse'] = [] for index in range(len(self.config['fullname_parse_regex'])): self.config['fullname_parse'].append(re.compile(self.config['fullname_parse_regex'][index] % self.config['fullname_parse_season_episode_translation'], re.UNICODE)) if self.config['mythtvmeta']: if mythdb == None or mythvideo == None: sys.stderr.write(u"\n! Error: The MythTV python interface is not installed or Cannot connect to MythTV Backend. MythTV meta data cannot be updated\n\n") sys.exit(1) try: import Image self.config['image_library'] = Image except Exception, e: sys.stderr.write(u"""\n! Error: Python Imaging Library is required for figuring out the sizes of the fetched poster images. In Debian/Ubuntu it is packaged as 'python-imaging'. http://www.pythonware.com/products/pil/\nError:(%s)\n""" % e) sys.exit(1) if not _can_int(self.config['min_poster_size']): sys.stderr.write(u"\n! Error: The poster minimum value must be an integer (%s)\n" % self.config['min_poster_size']) sys.exit(1) else: self.config['min_poster_size'] = int(self.config['min_poster_size']) if self.config['maximum'] != None: if _can_int(self.config['maximum']) == False: sys.stderr.write(u"\n! Error: Maximum option is not an integer (%s)\n" % self.config['maximum']) sys.exit(1) # Detect if this is a move request self.config[u'file_move_flag'] = False if len(args) != 0: if os.path.isfile(args[0]) or os.path.isdir(args[0]) or args[0][-1:] == '*': self.config[u'file_move_flag'] = True self.args = list(args) if self.config['mythtvdir']: if mythdb == None or mythvideo == None: sys.stderr.write(u"\n! Error: MythTV python interface is not available\n") sys.exit(1) if self.config['mythtvdir'] or self.config['mythtvmeta']: self._addMythtvUserFileTypes() # add user filetypes from the "videotypes" table self._getMythtvDirectories() if self.config['mythtvjanitor']: # Check for graphic directory conflicts with other plugins if self._JanitorConflicts(): sys.exit(1) if not self.config['mythtvNFS']: global graphicsDirectories, image_extensions dirs = [] for key in graphicsDirectories: if key != u'screenshot': for directory in self.config[graphicsDirectories[key]]: dirs.append(directory) # Check if any Graphics files are on NFS shares if self._checkNFS(dirs, image_extensions): sys.stderr.write(u"\n! Error: Your metadata graphics reside on a NFS mount.\nIn the case where you have more than one MythTV backend using the same directories to store your graphics\nthe Jamu's Janitor option (-MJ) will be destructive removing graphics used by the other backend(s).\n\nIf you only have one backend or do not mix the graphics directories between backends and still want to use\nJamu's Janitor use the options (-MJN) which will skip this check.\n\n") sys.exit(1) if self.config['posterresize'] != False or self.config['fanartresize'] != False: if _useImageMagick("-version"): sys.stderr.write(u"\n! Error: ImageMagick is not installed, graphics cannot be resized. posterresize(%s), fanartresize(%s)\n" % (str(self.config['posterresize']), str(self.config['fanartresize']))) sys.exit(1) if self.config['mythtvmeta'] and len(args) == 0: args=[''] if len(args) == 0: sys.stderr.write(u"\n! Error: At least a video directory, SID or season name must be supplied\n") sys.exit(1) if os.path.isfile(args[0]) or os.path.isdir(args[0]) or args[0][-1:] == '*': self.config['video_dir'] = [] for arg in args: self.config['video_dir'].append(unicode(arg,'utf8')) elif not self.config['mythtvmeta']: if _can_int(args[0]) and len(args[0]) >= 5: self.config['sid'] = unicode(args[0], 'utf8') # There is still a chance that this is a series name "90210" else: if self.config['series_name_override']: if self.config['series_name_override'].has_key(args[0].lower()): self.config['sid'] = unicode((self.config['series_name_override'][args[0].lower()]).strip(), 'utf8') else: self.config['series_name'] = unicode(args[0].strip(), 'utf8') else: self.config['series_name'] = unicode(args[0].strip(), 'utf8') if len(args) != 1: if len(args) > 3: sys.stderr.write("\n! Error: Too many arguments (%d), maximum is three.\n" % len(args)) print "! args:", args sys.exit(1) if len(args) == 3 and _can_int(args[1]) and _can_int(args[2]): self.config['season_num'] = args[1] self.config['episode_num'] = args[2] elif len(args) == 3: sys.stderr.write(u"\n! Error: Season name(%s), season number(%s), episode number (%s) combination is invalid\n" % (args[0], args[1], args[2])) sys.exit(1) elif len(args) == 2 and _can_int(args[1]): self.config['season_num'] = args[1] else: if self.config['ep_name_massage']: if self.config['ep_name_massage'].has_key(self.config['series_name']): tmp_ep_name=args[1].strip() tmp_array=self.config['ep_name_massage'][self.config['series_name']] for pair in tmp_array: tmp_ep_name = tmp_ep_name.replace(pair[0],pair[1]) self.config['episode_name'] = unicode(tmp_ep_name, 'utf8') else: self.config['episode_name'] = unicode(args[1].strip(), 'utf8') else: self.config['episode_name'] = unicode(args[1].strip(), 'utf8') # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml valid_languages = ["da", "fi", "nl", "de", "it", "es", "fr","pl", "hu","el","tr", "ru","he","ja","pt","zh","cs","sl", "hr","ko","en","sv","no"] # Validate language as specified by user if self.config['local_language']: if not self.config['local_language'] in valid_languages: valid_langs = '' for lang in valid_languages: valid_langs+= lang+', ' valid_langs=valid_langs[:-2] sys.stderr.write(u"\n! Error: Specified language(%s) must match one of the following languages supported by thetvdb.com wiki:\n (%s)\n" % (self.config['local_language'], valid_langs)) sys.exit(1) global UI_search_language UI_search_language = self.config['local_language'] if self.config['data_flags']: for data_type in self.config['data_flags']: if self.data_flags_table.has_key(data_type): self.config[self.data_flags_table[data_type]]=True # end validate_setVariables def __repr__(self): """Return a copy of the configuration variables """ return self.config #end __repr__ # end class Configuration class Tvdatabase(object): """Process direct thetvdb.com requests """ def __init__(self, configuration): """Retrieve all configuration options and get an instance of tvdb_api which is used to access thetvdb.com wiki. """ self.config = configuration cache_dir=u"/tmp/tvdb_api_%s/" % os.geteuid() if self.config['interactive']: self.config['tvdb_api'] = tvdb_api.Tvdb(banners=True, debug=self.config['debug_enabled'], interactive=True, select_first=False, cache=cache_dir, actors = True, language = self.config['local_language'], custom_ui=jamu_ConsoleUI, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV) else: self.config['tvdb_api'] = tvdb_api.Tvdb(banners=True, debug = self.config['debug_enabled'], cache = cache_dir, actors = True, language = self.config['local_language'], apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV) # Local variables # High level dictionay keys for select graphics URL(s) fanart_key=u'fanart' banner_key=u'series' poster_key=u'poster' season_key=u'season' # Lower level dictionay keys for select graphics URL(s) poster_series_key=u'680x1000' poster_season_key=u'season' fanart_hires_key=u'1920x1080' fanart_lowres_key=u'1280x720' banner_series_key=u'graphical' banner_season_key=u'seasonwide' # Type of graphics being requested poster_type=u'poster' fanart_type=u'fanart' banner_type=u'banner' ep_image_type=u'filename' def sanitiseFileName(self, name): '''Take a file name and change it so that invalid or problematic characters are substituted with a "_" return a sanitised valid file name ''' if name == None or name == u'': return u'_' for char in self.config['filename_char_filter']: name = name.replace(char, u'_') if name[0] == u'.': name = u'_'+name[1:] return name # end sanitiseFileName() def _getSeriesBySid(self, sid): """Lookup a series via it's sid return tvdb_api Show instance """ seriesid = u'sid:' + sid if not self.corrections.has_key(seriesid): self._getShowData(sid) self.corrections[seriesid] = sid return self.shows[sid] tvdb_api.Tvdb.series_by_sid = _getSeriesBySid # end _getSeriesBySid def _searchforSeries(self, sid_or_name): """Get TV series data by sid or series name return None if the TV show was not found return an tvdb_api instance of the TV show data if it was found """ if self.config['sid']: show = self.config['tvdb_api'].series_by_sid(self.config['sid']) if len(show) != 0: self.config['series_name']=show[u'seriesname'] return show else: if self.config['series_name_override']: if self.config['series_name_override'].has_key(sid_or_name.lower()): self.config['sid'] = (self.config['series_name_override'][sid_or_name.lower()]) show = self.config['tvdb_api'].series_by_sid(self.config['sid']) if len(show) != 0: self.config['series_name'] = show[u'seriesname'] return show else: show = self.config['tvdb_api'][sid_or_name] if len(show) != 0: self.config['series_name'] = show[u'seriesname'] return show else: show = self.config['tvdb_api'][sid_or_name] if len(show) != 0: self.config['series_name'] = show[u'seriesname'] return show # end _searchforSeries def verifySeriesExists(self): """Verify that a: Series or Series and Season or Series and Season and Episode number or Series and Episode name passed by the user exists on thetvdb.com return False and display an appropriate error if the TV data was not found return an tvdb_api instance of the TV show/season/episode data if it was found """ sid=self.config['sid'] series_name=self.config['series_name'] season=self.config['season_num'] episode=self.config['episode_num'] episode_name=self.config['episode_name'] try: self.config['log'].debug(u'Checking for series(%s), sid(%s), season(%s), episode(%s), episode name(%s)' % (series_name, sid, season, episode, episode_name)) if episode_name: # Find an exact match for the series and episode name series_sid='' if sid: seriesfound=self._searchforSeries(sid).search(episode_name) else: seriesfound=self._searchforSeries(series_name).search(episode_name) if len(seriesfound) != 0: for ep in seriesfound: if ep['seriesid'] == '999999999': self.config['sid'] = ep['seriesid'] return(ep) if (ep['episodename'].lower()).startswith(episode_name.lower()): if len(ep['episodename']) > (len(episode_name)+1): # Skip episodes the are not part of a set of (1), (2) ... etc if ep['episodename'][len(episode_name):len(episode_name)+2] != ' (': continue series_sid = ep['seriesid'] self.config['sid'] = ep['seriesid'] self.config['season_num'] = ep['seasonnumber'] self.config['episode_num'] = ep['episodenumber'] return(ep) else: # Exact match series_sid = ep['seriesid'] self.config['sid'] = ep['seriesid'] self.config['season_num'] = ep['seasonnumber'] self.config['episode_num'] = ep['episodenumber'] return(ep) raise tvdb_episodenotfound # Search for the series or series & season or series & season & episode elif season: if episode: # series & season & episode seriesfound=self._searchforSeries(series_name)[int(season)][int(episode)] if seriesfound['seriesid'] == '999999999': return(seriesfound) self.config['sid'] = seriesfound['seriesid'] self.config['episode_name'] = seriesfound['episodename'] else: # series & season seriesfound=self._searchforSeries(series_name)[int(season)] else: seriesfound=self._searchforSeries(series_name) # Series only except tvdb_shownotfound: # No such show found. # Use the show-name from the files name, and None as the ep name if series_name: sys.stderr.write(u"\n! Warning: Series (%s) not found\n" % ( series_name ) ) else: sys.stderr.write(u"\n! Warning: Series TVDB number (%s) not found\n" % ( sid ) ) return(False) except (tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound): # The season, episode or name wasn't found, but the show was. # Use the corrected show-name, but no episode name. if series_name == None: series_name = sid if episode: sys.stderr.write(u"\n! Warning: For Series (%s), season (%s) or Episode (%s) not found \n" % (series_name, season, episode ) ) elif episode_name: sys.stderr.write(u"\n! Warning: For Series (%s), Episode (%s) not found \n" % (series_name, episode_name ) ) else: sys.stderr.write(u"\n! Warning: For Series (%s), season (%s) not found \n" % ( series_name, season) ) return(False) except tvdb_error, errormsg: # Error communicating with thetvdb.com if sid: # Maybe the 5 digit number was a series name (e.g. 90210) self.config['series_name']=self.config['sid'] self.config['sid'] = None return self.verifySeriesExists() sys.stderr.write( u"\n! Warning: Error contacting www.thetvdb.com:\n%s\n" % (errormsg) ) return(False) except tvdb_userabort, errormsg: # User aborted selection (q or ^c) print "\n", errormsg return(False) else: return(seriesfound) # end verifySeriesExists def _resizeGraphic(self, filename, resize): """Resize a graphics file return False and display an error message if the graphics resizing failed return True if the resize was succcessful """ if self.config['simulation']: sys.stdout.write( u"Simulation resize command (mogrify -resize %s %s)\n" % (resize, filename) ) return(True) if _useImageMagick('-resize %s "%s"' % (resize, filename)): sys.stderr.write( u'\n! Warning: Resizing failed command (mogrify -resize %s "%s")\n' % (resize, filename) ) return(False) return True # end _resizeGraphic def _downloadURL(self, url, OutputFileName): """Download the specified graphic file from a URL return False if no file was downloaded return True if a file was successfully downloaded """ # Only download a file if it does not exist or the option overwrite is selected if not self.config['overwrite'] and os.path.isfile(OutputFileName): return False if self.config['simulation']: sys.stdout.write( u"Simulation download of URL(%s) to File(%s)\n" % (url, OutputFileName) ) return(True) org_url = url tmp_URL = url.replace("http://", "") url = "http://"+urllib.quote(tmp_URL.encode("utf-8")) try: dat = urllib.urlopen(url).read() except IOError, e: sys.stderr.write( u"\n! Warning: Download IOError on URL for Filename(%s)\nOrginal URL(%s)\nIOError urllib.quote URL(%s)\nError:(%s)\n" % (OutputFileName, org_url, url, e)) return False try: target_socket = open(OutputFileName, "wb") target_socket.write(dat) target_socket.close() except IOError, e: sys.stderr.write( u"\n! Warning: Download IOError for Filename(%s), may be the directory is invalid\nError:(%s)\n" % (OutputFileName, e)) return False # Verify that the downloaded file was NOT HTML instead of the intended file try: p = subprocess.Popen(u'file "%s"' % OutputFileName, shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) except Exception, e: sys.stderr.write( u"\n! Warning: Download Exception for Filename(%s)\nError:(%s)\n" % (OutputFileName, e)) return False except: return False data = p.stdout.readline() try: data = data.encode('utf8') except UnicodeDecodeError: data = unicode(data,'utf8') index = data.find(u'HTML document text') if index == -1: return True else: os.remove(OutputFileName) # Delete the useless HTML text if self.config['mythtv_verbose']: sys.stderr.write( u"\n! Warning: The web site may be having issues.\nURL (%s)\nReturned a file containing HTML\n(%s).\nThe bad downloaded file was removed.\n" % (url, OutputFileName)) return False # end _downloadURL def _setGraphicsFileNameFormat(self): """Return a file name format (e.g. seriesname - episode name.extention) return a filename format string """ if self.config['g_defaultname']: return u'%(url)s.%(ext)s' cfile={} cfile['seriesid']=self.config['sid'] cfile['series'] = self.sanitiseFileName(self.config['series_name']) if cfile['series'] != self.config['series_name']: self.config['g_series'] = self.config['g_series'].replace(self.config['series_name'], cfile['series']) if self.config['season_num']: cfile['seasonnumber']=int(self.config['season_num']) else: cfile['seasonnumber']=0 if self.config['episode_num']: cfile['episodenumber']=int(self.config['episode_num']) else: cfile['episodenumber']=0 cfile['episodename']=self.config['episode_name'] cfile['seq']=u'%(seq)02d' cfile['ext']=u'%(ext)s' if self.config['season_num']: return self.config['g_season'] % cfile return self.config['g_series'] % cfile # end _setGraphicsFileNameFormat def _downloadGraphics(self, urls, mythtv=False): """Download graphic file(s) from a URL list (string of one or more URLs separated by a CR character) return None is the string of urls has no urls return False if the any of the urls are corrupt return file name of the LAST file downloaded (special for MythTV data base updates) """ global graphicsDirectories if urls == None: return None if urls == '': return None tmp_list=urls.split('\n') url_list=[] for x in tmp_list: x = x.rstrip() if x != '': url_list.append(x) if not len(url_list): return None # There were no URLs in the list url_dict={} for x in url_list: try: self.config['log'].debug(u'Checking for a key in (%s)' % (x)) i = x.index(':') except: sys.stderr.write( u"\n! Warning: URL list does not have a graphics type key(%s)\n" % (x) ) return(False) if url_dict.has_key(x[:i]): temp_array = [x[i+1:],''] url_dict[x[:i]].append(temp_array)# Collect a list of the same graphics type of URLs else: # The first URL of a new graphics type. Also URL replacement code left in place just in case url_dict[x[:i]]=[[(x[i+1:]).replace(u"http://www.thetvdb.com",u"http://www.thetvdb.com"),'']] unique_dir={u'poster': ['posterdir', True], u'banner': ['bannerdir', True], u'fanart': ['fanartdir', True], u'filename': ['episodeimagedir', True]} # If a graphics directory was not specified then default to the 'allgraphics' directory if not self.config['posterdir']: self.config['posterdir'] = self.config['allgraphicsdir'] if not self.config['bannerdir']: self.config['bannerdir'] = self.config['allgraphicsdir'] if not self.config['fanartdir']: self.config['fanartdir'] = self.config['allgraphicsdir'] if not self.config['episodeimagedir']: self.config['episodeimagedir'] = self.config['allgraphicsdir'] # Check if any of the downloaded graphics will share the same directory for key in unique_dir.keys(): for k in unique_dir.keys(): if key != k: if self.config[unique_dir[key][0]] == self.config[unique_dir[k][0]]: unique_dir[key][1] = False break dirs={u'poster': self.config['posterdir'], u'banner': self.config['bannerdir'], u'fanart': self.config['fanartdir'], u'filename': self.config['episodeimagedir']} # Figure out filenaming convention file_format = self._setGraphicsFileNameFormat() # Set the graphics fully qualified filenames matched to a URL for URLtype in url_dict: if mythtv: if self.absolutepath: if URLtype == 'poster': tmpgraphicdir = graphicsDirectories['coverfile'] else: tmpgraphicdir = graphicsDirectories[URLtype] if not len(self.config['localpaths'][tmpgraphicdir]): return None else: directory = self.config['localpaths'][tmpgraphicdir][0] else: directory = dirs[URLtype][0] else: directory = dirs[URLtype] seq_num = 0 for url in url_dict[URLtype]: (dirName, fileName) = os.path.split(url[0]) (fileBaseName, fileExtension)=os.path.splitext(fileName) fileBaseName = self.sanitiseFileName(fileBaseName) # Fix file extentions in all caps or 4 character JPEG extentions fileExtension = fileExtension.lower() if fileExtension == '.jpeg': fileExtension = '.jpg' cfile={u'url': fileBaseName, u'seq': seq_num, u'ext': fileExtension[1:]} if not isValidPosixFilename(self.config['series_name']): if file_format.startswith(self.config['series_name']): file_format = file_format.replace(self.config['series_name'], self.sanitiseFileName(self.config['series_name'])) cfile['series'] = self.sanitiseFileName(self.config['series_name']) cfile['seriesid'] = self.config['sid'] if URLtype != 'filename': if unique_dir[URLtype][1]: url_dict[URLtype][seq_num][1] = directory+'/'+file_format % cfile else: if mythtv: url_dict[URLtype][seq_num][1] = directory+'/'+file_format % cfile else: url_dict[URLtype][seq_num][1] = directory+'/'+URLtype.capitalize()+' - '+file_format % cfile else: if self.config['season_num']: cfile['seasonnumber']=int(self.config['season_num']) else: cfile['seasonnumber'] = 0 if self.config['episode_num']: cfile['episodenumber']=int(self.config['episode_num']) else: cfile['episodenumber'] = 0 cfile['episodename'] = self.sanitiseFileName(self.config['episode_name']) url_dict[URLtype][seq_num][1] = directory+'/'+self.config['ep_metadata'] % cfile seq_num+=1 # Download the graphics and resize if requested - Ignore download or resize issues! failed_download = False for URLtype in url_dict: seq_num=0 for pairs in url_dict[URLtype]: if self._downloadURL(pairs[0], pairs[1]): if URLtype == u'poster' and self.config['posterresize']: self._resizeGraphic(pairs[1], self.config['posterresize']) elif URLtype == u'fanart' and self.config['fanartresize']: self._resizeGraphic(pairs[1], self.config['fanartresize']) elif not os.path.isfile(pairs[1]): # Check if the file already was downloaded failed_download = True # The download failed if self.config['mythtv_verbose']: sys.stderr.write(u'\nA graphics file failed to be downloaded. A file issue or a corrupt (HTML) file.(%s)\n' % pairs[1]) seq_num+=1 if self.config['maximum']: # Has the maximum number of graphics been downloaded? if seq_num == int(self.config['maximum']): break if failed_download: return None else: return pairs[1] # The name of the LAST graphics successfully downloaded # end _downloadGraphics def getGraphics(self, graphics_type): """Retrieve Poster or Fan Art or Banner or Episode image graphics URL(s) return None if no graphics URLs were found return a string of URLs """ banners=u'_banners' series_name=self.config['series_name'] season=self.config['season_num'] episode=self.config['episode_num'] episode_name=self.config['episode_name'] lang=self.config['local_language'] graphics=[] try: if self.config['sid']: URLs = self.config['tvdb_api'].ttvdb_parseBanners(self.config['sid']) else: URLs = self.config['tvdb_api'].ttvdb_parseBanners(self.config['tvdb_api']._nameToSid(series_name)) except Exception, e: return None if graphics_type == self.fanart_type: # Series fanart graphics if not len(URLs[u'fanart']): return None for url in URLs[u'fanart']: graphics.append(url) elif season == None and episode == None and episode_name == None: if not len(URLs[u'series']): return None if graphics_type == self.banner_type: # Series Banners for url in URLs[u'series']: graphics.append(url) else: # Series Posters for url in URLs[u'poster']: graphics.append(url) else: if not len(URLs[u'season']): return None if graphics_type == self.banner_type: # Season Banners season_banners=[] for url in URLs[u'season']: if url[u'bannertype2'] == u'seasonwide' and url[u'season'] == season: season_banners.append(url) if not len(season_banners): return None graphics = season_banners else: # Season Posters season_posters=[] for url in URLs[u'season']: if url[u'bannertype2'] == u'season' and url[u'season'] == season: season_posters.append(url) if not len(season_posters): return None graphics = season_posters graphicsURLs=u'' if self.config['nokeys'] and not self.config['download']: key_tag=u'' else: key_tag=graphics_type+u':' count = 0 wasanythingadded = 0 anyotherlanguagegraphics=u'' englishlanguagegraphics=u'' for URL in graphics: if graphics_type == 'filename': if URL[graphics_type] == None: continue if lang: # Is there a language to filter URLs on? if lang == URL['language']: if graphics_type != self.ep_image_type: graphicsURLs+=key_tag+URL['_bannerpath']+'\n' else: graphicsURLs+=key_tag+URL[graphics_type]+'\n' else: # Check for fall back graphics in case there are no selected language graphics if u'en' == URL['language']: if graphics_type != self.ep_image_type: englishlanguagegraphics+=key_tag+URL['_bannerpath']+'\n' else: englishlanguagegraphics+=key_tag+URL[graphics_type]+'\n' else: if graphics_type != self.ep_image_type: anyotherlanguagegraphics+=key_tag+URL['_bannerpath']+'\n' else: anyotherlanguagegraphics+=key_tag+URL[graphics_type]+'\n' else: if graphics_type != self.ep_image_type: graphicsURLs+=key_tag+URL['_bannerpath']+'\n' else: graphicsURLs+=key_tag+URL[graphics_type]+'\n' if wasanythingadded == len(graphicsURLs): continue wasanythingadded = len(graphicsURLs) count+=1 if self.config['maximum']: # Has the maximum number of graphics been downloaded? if count == int(self.config['maximum']): break if not len(graphicsURLs): if len(englishlanguagegraphics): # Fall back to English graphics graphicsURLs = englishlanguagegraphics elif len(anyotherlanguagegraphics): # Fall back-back to any available graphics graphicsURLs = anyotherlanguagegraphics if self.config['debug_enabled']: print "\nGraphics:\n", graphicsURLs if not len(graphicsURLs): # Are there any graphics? return None if len(graphicsURLs) == 1 and graphicsURLs[0] == graphics_type+':': return None # Due to the language filter there may not be any URLs return(graphicsURLs) # end get_graphics def getTopRatedGraphics(self, graphics_type): """Retrieve only the top rated series Poster, Fan Art and Banner graphics URL(s) return None if no top rated graphics URLs were found return a string of top rated URLs """ if graphics_type == u'filename': self.config['log'].debug(u'! There are no such thing as top rated Episode image URLs') return None toprated=None series_name=self.config['series_name'] keys=self.config['nokeys'] if self._searchforSeries(series_name)[graphics_type] != None: if keys and not self.config['download']: toprated=(self._searchforSeries(series_name)[graphics_type])+'\n' else: toprated=(u'%s:%s\n' % (graphics_type, self._searchforSeries(series_name)[graphics_type])) return toprated # end getTopRatedGraphics def _downloadEpisodeData(self,ep_data): """Down load episode meta data and episode image graphics return True whether or not there was episode data processed """ if not len(ep_data): return True # There were no episode data in the list ep_data_list=[] # An array of episode meta data first_key=self.config['ep_include_data'][0]+':' key_size=len(first_key) while len(ep_data): # Grab each episode's set of meta data try: self.config['log'].debug(u'Parse out the episode data from an episode meta dats string') end = ep_data[key_size:].index(first_key) ep_data_list.append(ep_data[:end+key_size]) ep_data=ep_data[end+key_size:] except: ep_data_list.append(ep_data) break if not self.config['metadatadir']: self.config['metadatadir'] = os.getcwd() # Process each episode's meta data for episode in ep_data_list: tmp_data = episode.split('\n') for i in range(len(tmp_data)): tmp_data[i] = tmp_data[i].rstrip()# Remove \n characters from the end of each record tmp_dict ={} for data in tmp_data: try: self.config['log'].debug(u'Checking for key in episode meta data') tmp_dict[data[:data.index(':')]] = data[data.index(':')+1:] except ValueError: continue tmp_dict['ext']='meta' tmp_dict['seq']=0 for key in ['seasonnumber', 'episodenumber']: if tmp_dict.has_key(key): tmp_dict[key] = int(tmp_dict[key]) if not tmp_dict.has_key(u'episodename'): tmp_dict[u'episodename'] = u'' filename="%s/%s" % (self.config['metadatadir'],self.config['ep_metadata'] % tmp_dict) image_filename = None if self.config['get_ep_image'] and tmp_dict.has_key('filename'): url= tmp_dict['filename'] if url != 'None': if not self.config['episodeimagedir']: self.config['episodeimagedir'] = self.config['allgraphicsdir'] (dirName, fileName) = os.path.split(url) (fileBaseName, fileExtension)=os.path.splitext(fileName) tmp_dict[u'ext']=fileExtension[1:] image_filename = "%s/%s" % (self.config['episodeimagedir'], self.config['ep_metadata'] % tmp_dict) # Only download a file if it does not exist or the option overwrite is selected # or the option update is selected and the local meta data file is # older than the episode data on thetvdb.com wiki outofdate = False if self.config['update'] and tmp_dict.has_key('lastupdated') and os.path.isfile(filename): if int(tmp_dict['lastupdated']) > int(os.path.getmtime(filename)): outofdate= True if not self.config['overwrite'] and not outofdate: if self.config['get_ep_meta'] and self.config['get_ep_image']: if image_filename: if os.path.isfile(filename) and os.path.isfile(image_filename): continue else: if os.path.isfile(filename): continue elif self.config['get_ep_meta']: if os.path.isfile(filename): continue elif self.config['get_ep_image'] and tmp_dict.has_key('filename'): url= tmp_dict['filename'] if url != 'None': if os.path.isfile(image_filename): continue else: continue else: continue if self.config['simulation']: if self.config['get_ep_image'] and tmp_dict.has_key('filename'): self.config['log'].debug(u'Simulate downloading an episode image') url= tmp_dict['filename'] if url != 'None': sys.stdout.write(u"Simulation create episode image file(%s)\n" % image_filename) if self.config['get_ep_meta']: sys.stdout.write( u"Simulation create meta data file(%s)\n" % filename ) continue if self.config['get_ep_image'] and tmp_dict.has_key('filename'): if tmp_dict['filename'] != 'None': self._downloadGraphics('filename:'+tmp_dict['filename']) # Write out an episode meta data file if self.config['get_ep_meta']: fHandle = codecs.open(filename, 'w', 'utf8') fHandle.write(episode) fHandle.close() return True # end _downloadEpisodeData def _changeToCommas(self,meta_data): """Remove '|' and replace with commas return the modified text """ if not meta_data: return meta_data meta_data = (u'|'.join([d for d in meta_data.split('| ') if d])) return (u', '.join([d for d in meta_data.split(u'|') if d])) # end _changeToCommas def _changeAmp(self, text): """Change & values to ASCII equivalents return the modified text """ if not text: return text text = text.replace(""", u"'").replace("\r\n", u" ") text = text.replace(r"\'", u"'") return text # end _changeAmp def getSeriesEpisodeData(self): """Get Series Episode meta data. This can be one specific episode or all of a seasons episodes or all episodes for an entire series. return an empy sting of no episode meta data was found reurn a string containing key value pairs of episode meta data """ sid=self.config['sid'] series_name=self.config['series_name'] season_num=self.config['season_num'] episode_num=self.config['episode_num'] episode_name=self.config['episode_name'] # Get Cast members tmp_cast={} cast_members='' try: tmp_cast = self._searchforSeries(series_name)[u'_actors'] except: cast_members='' if len(tmp_cast): cast_members='' for cast in tmp_cast: cast_members+=(cast['name']+u', ').encode('utf8') if cast_members != '': try: cast_members = cast_members[:-2].encode('utf8') except UnicodeDecodeError: cast_members = unicode(cast_members[:-2],'utf8') cast_members = self._changeAmp(cast_members) cast_members = self._changeToCommas(cast_members) cast_members=cast_members.replace('\n',' ') # Get genre(s) genres='' try: genres_string = self._searchforSeries(series_name)[u'genre'].encode('utf8') except: genres_string='' if genres_string != None and genres_string != '': genres = self._changeAmp(genres_string) genres = self._changeToCommas(genres) seasons=self._searchforSeries(series_name).keys() # Get the seasons for this series episodes_metadata=u'' for season in seasons: if season_num: # If a season was specified skip other seasons if season != int(season_num): continue episodes=self._searchforSeries(series_name)[season].keys()# Get the episodes for this season for episode in episodes: # If an episode was specified skip other episodes if episode_num: if episode != int(episode_num): continue ep_data={} if sid: # Ouput the full series name try: ep_data["series"]=self._searchforSeries(sid)[u'seriesname'].encode('utf8') except AttributeError: return u'' else: try: ep_data["series"]=self._searchforSeries(series_name)[u'seriesname'].encode('utf8') except AttributeError: return u'' available_keys=self._searchforSeries(series_name)[season][episode].keys() tmp=u'' ep_data[u'gueststars']='' for key in available_keys: if self._searchforSeries(series_name)[season][episode][key] == None: continue # Massage meta data text = self._searchforSeries(series_name)[season][episode][key] text = self._changeAmp(text) text = self._changeToCommas(text) ep_data[key.lower()]=text.replace('\n',' ') for key in self.config['ep_include_data']: # Select and sort the required meta data if ep_data.has_key(key): if key == u'gueststars': if ep_data[key] == '': tmp+=u'Cast:%s\n' % cast_members else: if (len(ep_data[key]) > 128) and not ep_data[key].count(','): tmp+=u'Cast:%s\n' % cast_members else: tmp+=u'Cast:%s, %s\n' % (cast_members, ep_data[key]) continue try: tmp+=u'%s:%s\n' % (key, ep_data[key]) except UnicodeDecodeError: tmp+=u'%s:%s\n' % (key, unicode(ep_data[key], "utf8")) tmp+=u'Runtime:%s\n' % self._searchforSeries(series_name)[u'runtime'] if genres != '': tmp+=u'Genres:%s\n' % genres if len(tmp) > 0: episodes_metadata+=tmp return episodes_metadata # end Getseries_episode_data def returnFilename(self): """Return a single file name (excluding file extension and directory), limited by the current variables (sid, season name, season number ... etc). Typically used when writing a meta file or naming/renaming a video file after a TV show recording. return False and out put an error if there not either a series id (SID) or series name return False and out put an error if there proper episode information (numbers or name) return False if the option (-MGF) used and there is not exact TV series name match return a specific episode filename """ sid=self.config['sid'] series_name=self.config['series_name'] season_num=self.config['season_num'] episode_num=self.config['episode_num'] episode_name=self.config['episode_name'] if not sid and not series_name: sys.stderr.write( u"\n! Warning: There must be at least series name or SID to request a filename\n" ) return False if season_num and episode_num: pass elif not episode_name: sys.stderr.write( u'\n! Error: There must be at least "season and episode numbers" or "episode name" to request a filename\n' ) sys.exit(1) # Special logic must be used if the (-MG) guessing option has been requested if not self.config['sid'] and self.config['mythtv_guess']: try: allmatchingseries = self.config['tvdb_api']._getSeries(self.config['series_name']) except Exception, e: sys.stderr.write(u"\nErrors while trying to contact thetvbd.com for Series (%s)\ntherefore a file rename is not possible. error(%s)\n\n" % (self.config['series_name'], e)) return False if filter(is_not_punct_char, allmatchingseries['name'].lower()) == filter(is_not_punct_char, self.config['series_name'].lower()): self.config['sid'] = allmatchingseries['sid'] self.config['series_name'] = allmatchingseries['name'] else: sys.stderr.write(u"\nGuessing could not find an exact match on tvdb for Series (%s)\ntherefore a file rename is not possible.\n\n" % self.config['series_name']) return False episode = self.verifySeriesExists() if not episode: # Make sure an episode was found sys.stderr.write( u'\n! Error: The episode was not found for series(%s), Episode name(%s)\n' % (series_name, episode_name) ) sys.exit(1) sid=self.config['sid'] if UI_selectedtitle and (self.config['mythtv_inetref'] or self.config['mythtv_ref_num']): self.config['series_name'] = UI_selectedtitle series_name=self.config['series_name'] season_num=self.config['season_num'] episode_num=self.config['episode_num'] episode_name=self.config['episode_name'] tmp_dict ={'series': series_name, 'seasonnumber': season_num, 'episodenumber': episode_num, 'episodename': episode_name, 'sid': sid } tmp_dict['ext']='' for key in ['seasonnumber', 'episodenumber']: if tmp_dict.has_key(key): tmp_dict[key] = int(tmp_dict[key]) return self.sanitiseFileName(u"%s" % (self.config['ep_metadata'] % tmp_dict)[:-1]) # end returnFilename def processTVdatabaseRequests(self): """Process the data/download requests as indicated by the variables return None if the series/season/episode does not exist return None if there is no data to process for the request actions return a string for display or further processing that satisfies the reqested actions """ if self.verifySeriesExists():# Getting a filename is a single event nothing else is returned if self.config['ret_filename']: return self.returnFilename() else: return None types={'get_fanart': self.fanart_type, 'get_poster': self.poster_type, 'get_banner': self.banner_type} if self.config['toprated']: typegetGraphics=self.getTopRatedGraphics else: typegetGraphics=self.getGraphics results=u'' if self.verifySeriesExists(): if self.config['download']: # Deal only with graphics display or downloads for key in types.keys(): if key == 'get_ep_image': # Ep image downloads processed below continue if self.config[key]: if self._downloadGraphics(typegetGraphics(types[key])): sys.stdout.write( u"%s downloading successfully processed\n" % key.title() ) else: url_string=u'' for key in types.keys(): if self.config[key]: string=typegetGraphics(types[key]) if string != None: url_string+=string if url_string != '': results+=url_string # Add graphic URLs to returned results # Should episode meta data or episode image be processed? if self.config['get_ep_meta'] or self.config['get_ep_image']: if self.config['download']: # Deal only with episode data display or download if self._downloadEpisodeData(self.getSeriesEpisodeData()): sys.stdout.write( u"Episode meta data and/or images downloads successfully processed\n" ) else: eps_string = self.getSeriesEpisodeData() if eps_string != '': results+=eps_string # Add episode meta data to returned results else: return None if results != u'': if results[len(results)-1] == '\n': return results[:len(results)-1] else: return results else: return None # end processTVdatabaseRequests def __repr__(self): # Just a place holder return self.config # end __repr__ # end Tvdatabase class VideoFiles(Tvdatabase): """Process all video file and/or directories containing video files. These TV Series video files must be named so that a "series name or sid" and/or "season and episode number" can be extracted from the video file name. It is best to have renamed the TV series video files with tvnamer before using these files with jamu. Any video file without season and episode numbers is assumed to be a movie. Files that do not match the previously described criterion will be skipped. tvnamer can be found at: http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search """ def __init__(self, configuration): """Retrieve the configuration options """ super(VideoFiles, self).__init__(configuration) # end __init__ image_extensions = ["png", "jpg", "bmp"] def _findFiles(self, args, recursive = False, verbose = False): """ Takes a file name or folder path and grabs files inside them. Does not recurse more than one level (if a folder is supplied, it will list files within), unless recurse is True, in which case it will recursively find all files. return an array of file names """ allfiles = [] for cfile in args: # Directories must exist and be both readable and writable if os.path.isdir(cfile) and not os.access(cfile, os.F_OK | os.R_OK): sys.stderr.write(u"\n! Error: Video directory (%s) does not exist or the permissions are not at least readable. Skipping this directory.\n" % (cfile)) continue ignore = False if os.path.isdir(cfile): for directory in self.config['ignore-directory']: # ignore directory list if not cfile.startswith(directory): continue ignore = True if ignore: # Skip this directory continue if os.path.isdir(cfile): index = cfile.find(u'VIDEO_TS') if index != -1: sys.stderr.write(u"\n! Warning: Jamu does not process multi-part video files, video directory (%s).\nSkipping this directory. Use MythVideo to retrieve meta data for these video files.\n" % (cfile)) continue try: cfile = unicode(cfile, u'utf8') except (UnicodeEncodeError, TypeError): pass for sf in os.listdir(cfile): try: newpath = os.path.join(cfile, sf) except: sys.stderr.write(u"\n! Error: This video file cannot be processed skipping:\n") sys.stderr.write(sf) sys.stderr.write(u"\nIt may be advisable to rename this file and try again.\n\n") continue if os.path.isfile(newpath): allfiles.append(newpath) else: if recursive: allfiles.extend( self._findFiles([newpath], recursive = recursive, verbose = verbose) ) #end if recursive #end if isfile #end for sf elif self.config[u'file_move_flag'] and not os.access(cfile, os.F_OK | os.R_OK | os.W_OK): sys.stderr.write(u"\n! Error: The Video file (%s) to be moved must have the read and write permissions. Skipping this video file.\n" % (cfile)) elif os.path.isfile(cfile) and os.access(cfile, os.F_OK | os.R_OK): allfiles.append(cfile) # Files must exist and be at least readable #end if isdir #end for cfile return allfiles #end findFiles def _processNames(self, names, verbose=False, movies=False): """ Takes list of names, runs them though the self.config['name_parse'] regex parsing strings to extract series name, season and episode numbers. Non-video files are skipped. return an array of dictionaries containing series name, season and episode numbers, file path and full filename and file extention. """ allEps = [] for f in names: filepath, filename = os.path.split( f ) filename, ext = os.path.splitext( filename ) # Remove leading . from extension ext = ext.replace(u".", u"", 1) self.config['log'].debug(u'Checking for a valid video filename extension') if not ext.lower() in self.config[u'video_file_exts']: for key in self.image_extensions: if key == ext: break else: sys.stderr.write(u"\n! Warning: Skipping non-video file name: (%s)\n" % (f)) continue match = None for r in self.config['name_parse']: match = r.match(filename) if match: break # If the filename does not match the default regular # expressions, try to match the file path + filename with the # extended fullpath regular expression so we can extract the # needed information out of the pathname if not match: for r in self.config['fullname_parse']: match = r.match(os.path.join(filepath, filename)) if match: break categories='' if match: self.config['log'].debug(u'matched reg:%s'%match.re.pattern) seriesname, seasno, epno = match.groups() #remove ._- characters from name (- removed only if next to end of line) seriesname = re.sub("[\._]|\-(?=$)", " ", seriesname).strip() # ramsi remove [en] tags seriesname = re.sub("(?:\[.*\])+", " ", seriesname).strip() seasno, epno = int(seasno), int(epno) if self.config['series_name_override']: if self.config['series_name_override'].has_key(seriesname.lower()): if len((self.config['series_name_override'][seriesname.lower()]).strip()) == 7: categories+=u', Movie' movie = filename if movie.endswith(self.config['hd_dvd']): movie = movie.replace(self.config['hd_dvd'], '') categories+=u', DVD' categories+=u', HD' else: if movie.endswith(self.config['dvd']): movie = movie.replace(self.config['dvd'], '') categories+=u', DVD' movie = re.sub("[\._]|\-(?=$)", " ", movie).strip() # ramsi remove [en] tags movie = re.sub("(?:\[.*\])+", " ", movie).strip() try: allEps.append({ 'file_seriesname':movie, 'seasno':0, 'epno':0, 'filepath':filepath, 'filename':filename, 'ext':ext, 'categories': categories }) except UnicodeDecodeError: allEps.append({ 'file_seriesname':unicode(movie,'utf8'), 'seasno':0, 'epno':0, 'filepath':unicode(filepath,'utf8'), 'filename':unicode(filename,'utf8'), 'ext':unicode(ext,'utf8'), 'categories': categories }) categories+=u', TV Series' try: allEps.append({ 'file_seriesname':seriesname, 'seasno':seasno, 'epno':epno, 'filepath':filepath, 'filename':filename, 'ext':ext, 'categories': categories }) except UnicodeDecodeError: allEps.append({ 'file_seriesname':unicode(seriesname,'utf8'), 'seasno':seasno, 'epno':epno, 'filepath':unicode(filepath,'utf8'), 'filename':unicode(filename,'utf8'), 'ext':unicode(ext,'utf8'), 'categories': categories }) else: if movies: # Account for " - On DVD" and " HD - On DVD" extra text on file names categories+=u', Movie' movie = filename if movie.endswith(self.config['hd_dvd']): movie = movie.replace(self.config['hd_dvd'], '') categories+=u', DVD' categories+=u', HD' else: if movie.endswith(self.config['dvd']): movie = movie.replace(self.config['dvd'], '') categories+=u', DVD' movie = re.sub("[\._]|\-(?=$)", " ", movie).strip() # ramsi remove [en] tags movie = re.sub("(?:\[.*\])+", " ", movie).strip() try: allEps.append({ 'file_seriesname':movie, 'seasno':0, 'epno':0, 'filepath':filepath, 'filename':filename, 'ext':ext, 'categories': categories }) except UnicodeDecodeError: allEps.append({ 'file_seriesname':unicode(movie,'utf8'), 'seasno':0, 'epno':0, 'filepath':unicode(filepath,'utf8'), 'filename':unicode(filename,'utf8'), 'ext':unicode(ext,'utf8'), 'categories': categories }) else: sys.stderr.write(u"\n! Warning: Skipping invalid name: %s\n" % (f)) #end for r #end for f return allEps #end processNames def processFileOrDirectory(self): '''This routine is NOT used for MythTV meta data processing. If directory path has been specified then create a list of files that qualify as video files / including recursed directories. Then parse the list of file names to determine (series, season number, ep number and ep name). Skip any video file that cannot be parsed for sufficient info. Loop through the list: > Check if the series, season, ... exists, skip with debug message if none found > Set variable with proper info: sid, series, season and episode numbers > Process the file's information per the variable to get graphics and or meta data return False and an error message and exist the script if there are no video files to process return None when all processing was complete return a string of file names if the "Filename" process option was True ''' filenames='' allFiles = self._findFiles(self.config['video_dir'], self.config['recursive'] , verbose = self.config['debug_enabled']) validFiles = self._processNames(allFiles, verbose = self.config['debug_enabled']) if len(validFiles) == 0: sys.stderr.write(u"\n! Error: No valid video files found\n") sys.exit(1) path_flag = self.config['metadatadir'] for cfile in validFiles: sys.stdout.write(u"# Processing %(file_seriesname)s (season: %(seasno)d, episode %(epno)d)\n" % (cfile)) self.config['sid']=None self.config['episode_name'] = None self.config['series_name']=cfile['file_seriesname'] self.config['season_num']=u"%d" % cfile['seasno'] self.config['episode_num']=u"%d" % cfile['epno'] if not path_flag: # If no metaddata directory specified then default to the video file dir self.config['metadatadir'] = cfile['filepath'] if self.verifySeriesExists(): self.config['log'].debug(u"Found series(%s) season(%s) episode(%s)" % (self.config['series_name'], self.config['season_num'], self.config['episode_num'])) if self.config['ret_filename']: returned = self.processTVdatabaseRequests() if returned != None and returned != False: filenames+=returned+'\n' else: self.processTVdatabaseRequests() else: sys.stderr.write(u"\n! Warning: Did not find series(%s) season(%s) episode(%s)\n" % (self.config['series_name'], self.config['season_num'], self.config['episode_num'])) self.config['log'].debug("# Done") if len(filenames) == 0: return None else: return filenames[:-1] # drop the last '\n' # end processFileOrDirectory def __repr__(self): # Just a place holder return self.config # end __repr__ # end VideoFiles class MythTvMetaData(VideoFiles): """Process all mythvideo video files, update the video files associated MythTV meta data. Download graphics for those video files from either thetvdb.com or themovie.com. Video file names for TV episodes must series name, season and episode numbers. The video file's movie name must be an exact match with a movie title in themoviedb.com or the MythTV database must have an entry for the video file with a TMDB or an IMDB number (db field 'intref'). """ def __init__(self, configuration): """Retrieve the configuration options """ super(MythTvMetaData, self).__init__(configuration) # end __init__ # Local variables # A dictionary of meta data keys and initialized values global graphicsDirectories movie_file_format=u"%s/%s.%s" graphic_suffix = {u'coverfile': u'_coverart', u'fanart': u'_fanart', u'banner': u'_banner'} graphic_name_suffix = u"%s/%s%s.%s" graphic_name_season_suffix = u"%s/%s Season %d%s.%s" def _getSubtitle(self, cfile): '''Get the MythTV subtitle (episode name) return None return episode name string ''' self.config['sid']=None self.config['episode_name'] = None self.config['series_name']=cfile['file_seriesname'] self.config['season_num']=u"%d" % cfile['seasno'] self.config['episode_num']=u"%d" % cfile['epno'] self.verifySeriesExists() return self.config['episode_name'] # end _getSubtitle def hashFile(self, name): '''Create metadata hash values for mythvideo files return a hash value return u'' if the was an error with the video file or the video file length was zero bytes ''' filename = self.rtnRelativePath(name, u'mythvideo') # Use the MythVideo hashing protocol when the video is in a storage groups if filename[0] != u'/': hash_value = FileOps(mythbeconn.hostname).getHash(filename, u'Videos') if hash_value == u'NULL': return u'' else: return hash_value # Use a local hashing routine when video is not in a Videos storage group # For original code: http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#Python try: longlongformat = 'q' # long long bytesize = struct.calcsize(longlongformat) f = open(name, "rb") filesize = os.path.getsize(name) hash = filesize if filesize < 65536 * 2: # Video file is too small return u'' for x in range(65536/bytesize): buffer = f.read(bytesize) (l_value,)= struct.unpack(longlongformat, buffer) hash += l_value hash = hash & 0xFFFFFFFFFFFFFFFF #to remain as 64bit number f.seek(max(0,filesize-65536),0) for x in range(65536/bytesize): buffer = f.read(bytesize) (l_value,)= struct.unpack(longlongformat, buffer) hash += l_value hash = hash & 0xFFFFFFFFFFFFFFFF f.close() returnedhash = "%016x" % hash return returnedhash except(IOError): # Accessing to this video file caused and error return u'' # end hashFile() def rtnRelativePath(self, abpath, filetype): '''Check if there is a Storage Group for the file type (video, coverfile, banner, fanart, screenshot) and form an apprioriate relative path and file name. return a relative path and file name return an absolute path and file name if there is no storage group for the file type ''' if abpath == None: return abpath # There is a chance that this is already a relative path or there is no Storage group for file type if not len(storagegroups): return abpath if not storagegroups.has_key(filetype) or abpath[0] != '/': return abpath # The file must already be in one of the directories specified by the file type's storage group for directory in storagegroups[filetype]: if abpath.startswith(directory): return abpath[len(directory)+1:] else: return abpath # end rtnRelativePath def rtnAbsolutePath(self, relpath, filetype): '''Check if there is a Storage Group for the file type (mythvideo, coverfile, banner, fanart, screenshot) and form an appropriate absolute path and file name. return an absolute path and file name return the relpath sting if the file does not actually exist in the absolute path location ''' if relpath == None or relpath == u'': return relpath # There is a chance that this is already an absolute path if relpath[0] == u'/': return relpath if self.absolutepath: if not len(self.config['localpaths'][filetype]): return relpath directories = self.config['localpaths'][filetype] else: directories = self.config[filetype] for directory in directories: abpath = u"%s/%s" % (directory, relpath) if os.path.isfile(abpath): # The file must actually exist locally return abpath else: return relpath # The relative path does not exist at all the metadata entry is useless # end rtnAbsolutePath def removeCommonWords(self, title): '''Remove common words from a title return title striped of common words ''' if not title: return u' ' wordList = [u'the ', u'a ', u' '] # common word list. Leave double space as the last value. title = title.lower() for word in wordList: title = title.replace(word, u'') if not title: return u' ' return filter(is_not_punct_char, title.strip()) # end removeCommonWords() def _getTmdbIMDB(self, title, watched=False, IMDB=False, rtnyear=False): '''Find and exact match of the movie name with what's on themoviedb.com If IMDB is True return an imdb# If rtnyear is True return IMDB# and the movie year in a dictionary return False (no matching movie found) return imdb# and/or tmdb# ''' global video_type, UI_title UI_title = title.replace(self.config[u'hd_dvd'], u'') UI_title = UI_title.replace(self.config[u'dvd'], u'') if UI_title[-1:] == ')': # Get rid of the (XXXX) year from the movie title tmp_title = UI_title[:-7].lower() else: tmp_title = UI_title.lower() if self.config['series_name_override']: if self.config['series_name_override'].has_key(tmp_title): return (self.config['series_name_override'][tmp_title]).strip() TMDB_movies=[] IMDB_movies=[] user_tmdb = False while True: try: if IMDB: results = [self.config['tmdb_api'].searchIMDB(IMDB)] elif user_tmdb: results = self.config['tmdb_api'].searchTMDB(user_tmdb) if rtnyear: if results.has_key('releasedate'): return {'name': "%s (%s)" % (results['title'], results['releasedate'][:4]), u'sid': results[u'inetref']} else: return {'name': "%s" % (results['title'], ), u'sid': results[u'inetref']} else: return results['inetref'] else: results = self.config['tmdb_api'].searchTitle(tmp_title) except TmdbMovieOrPersonNotFound, e: results = [{}] except Exception, errormsg: self._displayMessage(u"themoviedb.com error for Movie(%s) invalid data error (%s)" % (title, errormsg)) return False except: self._displayMessage(u"themoviedb.com error for Movie(%s)" % title) return False # Check if user's interactive response (Skip, selection, input #) if len(results[0]) and self.config['interactive']: if results[0].has_key('userResponse'): # Check if the user selected a specific movie from the list if results[0]['userResponse'] == 'User selected': if rtnyear: if results[0].has_key('released'): data = {'name': "%s (%s)" % (results[0]['name'], results[0]['released'][:4]), u'sid': results[0][u'id']} else: data = {'name': "%s" % (results[0]['name'], ), u'sid': results[0][u'id']} return data else: return results[0]['id'] # Check if the user has entered a TMDB number themselves if results[0]['userResponse'] == 'User input': user_tmdb = results[0]['id'] continue # Check if the user wants this video to be ignored by Jamu from now on if results[0]['id'] == '99999999': if rtnyear: return False else: return results[0]['id'] break if IMDB: # This is required to allow graphic file searching both by a TMDB and IMDB numbers if len(results[0]): if results[0].has_key('imdb_id'): return results[0]['imdb_id'][2:] else: return False else: return False if UI_title[-1:] == ')': name = UI_title[:-7].lower() # Just the movie title year = UI_title[-5:-1] # The movie release year else: name = tmp_title.lower() year = '' name = name.strip().replace(' ', ' ') if len(results[0]): for movie in results: if self.removeCommonWords(movie['name']) == self.removeCommonWords(name): if not year: if movie.has_key('released'): TMDB_movies.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}) else: TMDB_movies.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}) continue if movie.has_key(u'released'): if movie['released'][:4] == year: if rtnyear: return {'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']} else: return movie[u'id'] TMDB_movies.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}) continue else: TMDB_movies.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}) continue elif movie.has_key('alternative_name'): if self.removeCommonWords(movie['alternative_name']) == self.removeCommonWords(name): if not year: if movie.has_key('released'): TMDB_movies.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}) else: TMDB_movies.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}) continue if movie.has_key(u'released'): if movie['released'][:4] == year: if rtnyear: return {'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']} else: return movie['id'] TMDB_movies.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}) continue else: TMDB_movies.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}) continue # When there is only one match but NO year to confirm then it is OK to assume an exact match if len(TMDB_movies) == 1 and year == '': if rtnyear: return TMDB_movies[0] else: return TMDB_movies[0][u'sid'] if imdb_lib: # Can a imdb.com search be done? imdb_access = imdb.IMDb() movies_found = [] try: movies_found = imdb_access.search_movie(tmp_title.encode("ascii", 'ignore')) except Exception: return False if not len(movies_found): return False tmp_movies={} for movie in movies_found: # Get rid of duplicates try: # Protect against bad data from IMDBpy if movie.has_key('year'): temp = {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])} else: temp = {imdb_access.get_imdbID(movie): movie['title']} except Exception: return False if tmp_movies.has_key(temp.keys()[0]): continue tmp_movies[temp.keys()[0]] = temp[temp.keys()[0]] for movie in tmp_movies: if tmp_movies[movie][:-7].lower() == name or self.removeCommonWords(tmp_movies[movie][:-7]) == self.removeCommonWords(name): if year: if tmp_movies[movie][-5:-1] == year: if rtnyear: return {'name': tmp_movies[movie], u'sid': movie} else: return u"%07d" % int(movie) # Pad out IMDB# with leading zeroes IMDB_movies.append({'name': tmp_movies[movie], u'sid': movie}) if len(IMDB_movies) == 1: # If this is the only choice and titles matched then auto pick it if self.removeCommonWords(IMDB_movies[0]['name'][:-7]) == self.removeCommonWords(name): if rtnyear: return IMDB_movies[0] else: return u"%07d" % int(IMDB_movies[0][u'sid']) # Does IMDB list this movie? if len(IMDB_movies) == 0: return False # Did the user want an interactive interface? if not self.config['interactive']: return False # Force only an IMDB look up for a movie movies = IMDB_movies video_type=u'IMDB' ui = jamu_ConsoleUI(config = self.config, log = self.config['log']) try: inetref = ui.selectSeries(movies) except tvdb_userabort: if video_type==u'IMDB' or len(IMDB_movies) == 0: self._displayMessage(u"1-No selection made for Movie(%s)" % title) return False movies = IMDB_movies video_type=u'IMDB' try: inetref = ui.selectSeries(movies) except tvdb_userabort: self._displayMessage(u"2-No selection made for Movie(%s)" % title) return False if inetref.has_key('sid'): if _can_int(inetref['sid']): if inetref['sid'] == '99999999': return inetref['sid'] if rtnyear: if inetref['name'] == u'User input': try: data = imdb_access.get_movie(inetref['sid']) if data.has_key('long imdb title'): return {'name': data['long imdb title'], u'sid': inetref['sid']} elif data.has_key('title'): return {'name': data['title'], u'sid': inetref['sid']} else: return False except imdb._exceptions.IMDbDataAccessError: return False else: return inetref else: return u"%07d" % int(inetref['sid']) # Pad out IMDB# with leading zeroes else: return False else: return False # end _getTmdbIMDB def _getTmdbGraphics(self, cfile, graphic_type, watched=False): '''Download either a movie Poster or Fanart return None return full qualified path and filename of downloaded graphic ''' if graphic_type == u'-P': graphic_name = u'poster' key_type = u'coverart' rel_type = u'coverfile' else: graphic_name = u'fanart' key_type = u'fanart' rel_type = key_type self.config['series_name']=cfile['file_seriesname'] try: if len(cfile['inetref']) == 7: # IMDB number results = self.config['tmdb_api'].searchIMDB(cfile['inetref']) else: results = self.config['tmdb_api'].searchTMDB(cfile['inetref']) except TmdbMovieOrPersonNotFound, e: self._displayMessage(u"0-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref'])) return None except Exception, e: self._displayMessage(u"themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile['file_seriesname'], graphic_name, e)) return None if results != None: if not results.has_key(key_type): self._displayMessage(u"1-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref'])) return None else: self._displayMessage(u"1b-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref'])) return None graphic_file = (results[key_type].split(u','))[0].strip() # Only want the first image URL self.config['g_defaultname']=False self.config['toprated'] = True self.config['nokeys'] = False self.config['sid']=None if watched: if self.program_seriesid == None: self.config['g_series'] = self.sanitiseFileName(cfile['file_seriesname'])+u' Season 1'+self.graphic_suffix[rel_type]+u'.%(ext)s' else: self.config['g_series'] = self.sanitiseFileName(self.program_seriesid)+self.graphic_suffix[rel_type]+u'.%(ext)s' else: self.config['g_series'] = cfile['inetref']+self.graphic_suffix[rel_type]+u'.%(ext)s' if graphic_type == '-P': g_type = u'poster' else: g_type = u'fanart' self.config['season_num']= None # Needed to get graphics named in 'g_series' format self.config['overwrite'] = True # Force overwriting any existing graphic file tmp_URL = graphic_file.replace(u"http://", u"") graphic_file = u"http://"+urllib.quote(tmp_URL.encode("utf-8")) value = self._downloadGraphics(u"%s:%s" % (g_type, graphic_file), mythtv=True) self.config['overwrite'] = False # Turn off overwriting if value == None: self._displayMessage(u"2-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref'])) return None else: return self.rtnRelativePath(value, graphicsDirectories[rel_type]) # end _getTmdbGraphics def _getSecondarySourceGraphics(self, cfile, graphic_type, watched=False): '''Download from secondary source such as movieposter.com return None return full qualified path and filename of downloaded graphic ''' if not len(self.config['myth_secondary_sources']): return None if graphic_type == u'coverfile': graphic_type = u'poster' rel_type = u'coverfile' if cfile['seasno'] == 0 and cfile['epno'] == 0: if not self.config['myth_secondary_sources'].has_key('movies'): return None if self.config['myth_secondary_sources']['movies'].has_key(graphic_type): source = self.config['myth_secondary_sources']['movies'][graphic_type] if source.find(u'%(imdb)s') != -1: if len(cfile['inetref']) != 7: try: results = self.config['tmdb_api'].searchTMDB(cfile['inetref']) except TmdbMovieOrPersonNotFound, e: self._displayMessage(u"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile['file_seriesname'], graphic_type, e)) return None except Exception, e: self._displayMessage(u"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile['file_seriesname'], graphic_type, e)) return None if results == None: return None if not results.has_key('imdb'): self._displayMessage(u"\n! Warning: themoviedb.com wiki does not have an IMDB number to search a secondary source (%s)\nfor the movie (%s) inetref (%s).\n" % (source , cfile['filename'], cfile['inetref'])) return None cfile['imdb'] = results['imdb'] else: cfile['imdb'] = cfile['inetref'] else: return None else: if not self.config['myth_secondary_sources'].has_key('tv'): return None if self.config['myth_secondary_sources']['tv'].has_key(graphic_type): source = self.config['myth_secondary_sources']['tv'][graphic_type] else: return None self.config['series_name']=cfile['file_seriesname'] if self.config['simulation']: sys.stdout.write(u"Simulating - downloading Secondary Source graphic (%s)\n" % cfile['file_seriesname']) return u"Simulated Secondary Source graphic filename place holder" # Test that the secondary's required data has been passed try: command = source % cfile except: self._displayMessage(u"Graphics Secondary source command:\n%s\nRequired information is not available. Here are the variables that are available:\n%s\n" % (source, cfile)) return None tmp_files = callCommandLine(command) if tmp_files == '': self._displayMessage(u"\n! Warning: Source (%s)\n could not find (%s) for (%s)(%s)\n" % (source % cfile, graphic_type, cfile['filename'], cfile['inetref'])) return None tmp_array=tmp_files.split('\n') if tmp_array[0].startswith(u'Failed'): self._displayMessage(u"\n! Warning: Source (%s)\nfailed to download (%s) for (%s)(%s)\n" % (source % cfile, graphic_type, cfile['filename'], cfile['inetref'])) return None if tmp_array[0].startswith(u'file://'): tmp_files=tmp_array[0].replace(u'file://', u'') if not os.path.isfile(tmp_files): sys.stderr.write(u'\n! Error: The graphic file does not exist (%s)\n' % tmp_files) sys.exit(1) # Fix file extentions in all caps or 4 character JPEG extentions fileExtension = (_getExtention(tmp_files)).lower() if fileExtension == u'jpeg': fileExtension = u'jpg' if watched: if self.program_seriesid == None: filename = u'%s/%s%s.%s' % (self.config['posterdir'][0], self.sanitiseFileName(cfile['file_seriesname']), self.graphic_suffix[rel_type], fileExtension) else: filename = u'%s/%s%s.%s' % (self.config['posterdir'][0], self.sanitiseFileName(self.program_seriesid), self.graphic_suffix[rel_type], fileExtension) else: filename = u'%s/%s%s.%s' % (self.config['posterdir'][0], cfile['inetref'], self.graphic_suffix[rel_type], fileExtension) if os.path.isfile(filename): # This may be the same small file or worse then current try: (width, height) = self.config['image_library'].open(filename).size (width2, height2) = self.config['image_library'].open(tmp_files).size if width >= width2: os.remove(tmp_files) return None except IOError: return None # Verify that the downloaded file was NOT HTML instead of the intended file if self._checkValidGraphicFile(tmp_files, graphicstype=u'', vidintid=False) == False: os.remove(tmp_files) # Delete the useless HTML text return None shutil.copy2(tmp_files, filename) os.remove(tmp_files) self.num_secondary_source_graphics_downloaded+=1 return self.rtnRelativePath(filename, graphicsDirectories[rel_type]) else: graphic_file = tmp_array[0] self.config['g_defaultname']=False self.config['toprated'] = True self.config['nokeys'] = False self.config['sid']=None if watched: if self.program_seriesid == None: self.config['g_series'] = self.sanitiseFileName(cfile['file_seriesname'])+self.graphic_suffix[rel_type]+'.%(ext)s' else: self.config['g_series'] = self.sanitiseFileName(self.program_seriesid)+self.graphic_suffix[rel_type]+'.%(ext)s' else: self.config['g_series'] = self.sanitiseFileName(cfile['inetref'])+self.graphic_suffix[rel_type]+'.%(ext)s' g_type = graphic_type self.config['season_num']= None # Needed to get graphics named in 'g_series' format self.config['overwrite'] = True # Force overwriting any existing graphic file tmp_URL = graphic_file.replace(u"http://", u"") graphic_file = u"http://"+urllib.quote(tmp_URL.encode("utf-8")) value = self._downloadGraphics(u"%s:%s" % (g_type, graphic_file), mythtv=True) self.config['overwrite'] = False # Turn off overwriting if value == None: self._displayMessage(u"Secondary source %s not found(%s)(%s)" % (graphic_file, cfile['filename'], cfile['inetref'])) return None else: self.num_secondary_source_graphics_downloaded+=1 return self.rtnRelativePath(value, graphicsDirectories[rel_type]) # end _getSecondarySourceGraphics def combineMetaData(self, available_metadata, meta_dict, vid_type=False): ''' Combine the current data with new meta data from primary or secondary sources return combinted meta data dictionary ''' # Combine meta data for key in meta_dict.keys(): if key in self.config['metadata_exclude_as_update_trigger']: continue else: if key == 'inetref' and available_metadata[key] != meta_dict[key]: available_metadata[key] = meta_dict[key] continue if key == 'releasedate' and available_metadata[key] != meta_dict[key]: available_metadata[key] = meta_dict[key] continue if key == 'userrating' and available_metadata[key] == 0.0: available_metadata[key] = meta_dict[key] continue if key == 'length' and available_metadata[key] == 0: available_metadata[key] = meta_dict[key] continue if key == 'rating' and (available_metadata[key] == 'NR' or available_metadata[key] == 'Unknown'): available_metadata[key] = meta_dict[key] continue if key == 'year' and available_metadata[key] == 1895: available_metadata[key] = meta_dict[key] continue if key == 'category' and available_metadata[key] == 0: available_metadata[key] = meta_dict[key] continue if key == 'inetref' and available_metadata[key] == '00000000': available_metadata[key] = meta_dict[key] continue if key == 'title': available_metadata[key] = meta_dict[key] continue if vid_type and key == 'subtitle': # There are no subtitles in movies continue if key == 'plot': # Remove any line-feeds from the plot. Mythvideo does not expect them. meta_dict[key] = meta_dict[key].replace('\n', ' ') if (vid_type and key == 'plot') and (meta_dict[key].find('@') != -1 or len(meta_dict[key].split(' ')) < 10): continue if vid_type and key == 'plot': if available_metadata[key] != None: if len(available_metadata[key].split(' ')) < 10 and len(meta_dict[key].split(' ')) > 10: available_metadata[key] = meta_dict[key] continue if not available_metadata.has_key(key): # Mainly for Genre, Cast and Countries available_metadata[key] = meta_dict[key] continue if available_metadata[key] == None or available_metadata[key] == '' or available_metadata[key] == 'None' or available_metadata[key] == 'Unknown': available_metadata[key] = meta_dict[key] continue return available_metadata # end combineMetaData def _getSecondarySourceMetadata(self, cfile, available_metadata): '''Download meta data from secondary source return available_metadata (returns the current metadata unaltered) return dictionary of combined meta data ''' if not len(self.config['myth_secondary_sources']): return available_metadata if cfile['seasno'] == 0 and cfile['epno'] == 0: if not self.config['myth_secondary_sources'].has_key('movies'): return available_metadata movie = True if self.config['myth_secondary_sources']['movies'].has_key('metadata'): source = self.config['myth_secondary_sources']['movies']['metadata'] if source.find(u'%(imdb)s') != -1: if len(cfile['inetref']) != 7: try: results = self.config['tmdb_api'].searchTMDB(cfile['inetref']) except TmdbMovieOrPersonNotFound, e: self._displayMessage(u"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile['file_seriesname'], e)) return available_metadata except Exception, e: self._displayMessage(u"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile['file_seriesname'], e)) return available_metadata if results == None: return available_metadata if not results.has_key('imdb'): self._displayMessage(u"No IMDB number for meta data secondary source (%s)\nfor the movie (%s) inetref (%s) in themoviedb.com wiki.\n" % (source, cfile['filename'], cfile['inetref'])) return available_metadata cfile['imdb'] = results['imdb'] else: cfile['imdb'] = cfile['inetref'] else: return available_metadata else: if not self.config['myth_secondary_sources'].has_key('tv'): return available_metadata movie = False if self.config['myth_secondary_sources']['tv'].has_key('metadata'): source = self.config['myth_secondary_sources']['tv']['metadata'] else: return available_metadata # Test that the secondary's required data has been passed try: command = source % cfile except: self._displayMessage(u"Metadata Secondary source command:\n%s\nRequired information is not available. Here are the variables that are available:\n%s\n" % (source, cfile)) return available_metadata self.config['series_name']=cfile['file_seriesname'] tmp_files=u'' tmp_files = (callCommandLine(command)).decode("utf8") if tmp_files == '': self._displayMessage(u"1-Secondary source (%s)\ndid not find(%s)(%s) meta data dictionary cannot be returned" % (source % cfile, cfile['filename'], cfile['inetref'])) return available_metadata meta_dict={} tmp_array=tmp_files.split('\n') for element in tmp_array: element = (element.rstrip('\n')).strip() if element == '' or element == None: continue try: index = element.index(':') except: continue key = element[:index].lower() data = element[index+1:] if data == None or data == '': continue if key == u'inetref' and len(cfile['inetref']) == 7: meta_dict[key] = cfile['inetref'] continue data = self._changeAmp(data) data = self._changeToCommas(data) if key == 'year': try: meta_dict[key] = int(data) except: continue continue if key == 'userrating': try: meta_dict[key] = float(data) except: continue continue if key == 'runtime': try: meta_dict['length'] = long(data) except: continue continue if key == 'movierating': meta_dict['rating'] = data continue if key == 'plot': try: if len(data.split(' ')) < 10: # Skip plots that are less than 10 words continue except: pass if key == 'trailer': continue if key == 'releasedate': try: meta_dict[key] = datetime.datetime.strptime(data,'%Y-%m-%d').date() except ValueError: pass continue meta_dict[key] = data if not len(meta_dict): self._displayMessage(u"2-Secondary source (%s)\n did not find(%s)(%s) meta data dictionary cannot be returned" % (source % cfile, cfile['filename'], cfile['inetref'])) return available_metadata # Combine meta data available_metadata = self.combineMetaData(available_metadata, meta_dict, vid_type=movie) self.num_secondary_source_metadata_downloaded+=1 return available_metadata # end _getSecondarySourceMetadata def _getTmdbMetadata(self, cfile, available_metadata): '''Download a movie's meta data and massage the genres string return results for secondary sources when no primary source meta data return dictionary of metadata combined with data from a secondary source ''' try: if len(cfile['inetref']) == 7: # IMDB number meta_dict = self.config['tmdb_api'].searchIMDB(cfile['inetref']) else: meta_dict = self.config['tmdb_api'].searchTMDB(cfile['inetref']) except TmdbMovieOrPersonNotFound, e: self._displayMessage(u"0-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile['filename'], cfile['inetref'])) return self._getSecondarySourceMetadata(cfile, available_metadata) except Exception, e: self._displayMessage(u"themoviedb.com error for Movie(%s)(%s) meta data dictionary cannot be returned, error(%s)" % (cfile['filename'], cfile['inetref'], e)) return self._getSecondarySourceMetadata(cfile, available_metadata) if meta_dict == None: self._displayMessage(u"1-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile['filename'], cfile['inetref'])) return self._getSecondarySourceMetadata(cfile, available_metadata) keys = meta_dict.keys() for key in keys: data = meta_dict[key] if not data: continue if key == 'homepage': continue data = self._changeAmp(data) data = self._changeToCommas(data) if key == 'genres': genres='' genre_array = data.split(',') for i in range(len(genre_array)): genre_array[i] = (genre_array[i].strip()).lower() if genre_array[i] in self.config['tmdb_genre_filter']: genres+=genre_array[i].title()+',' if genres == '': meta_dict[key] = u'' continue else: meta_dict[key] = genres[:-1] if key == 'trailer': continue if key == 'year': try: meta_dict[key] = int(data) except: pass continue if key == 'userrating': try: meta_dict[key] = float(data) except: pass continue if key == 'url': meta_dict['homepage'] = data continue if key == 'releasedate': try: meta_dict[key] = datetime.datetime.strptime(data,'%Y-%m-%d').date() except ValueError: del meta_dict[key] continue if key == 'runtime': try: meta_dict['length'] = long(data) except: pass continue if key == 'movierating': meta_dict['rating'] = data continue if meta_dict.has_key('rating'): if meta_dict['rating'] == '': meta_dict['rating'] = 'Unknown' if len(meta_dict): if available_metadata['hash'] == u'' or available_metadata['hash'] == None: filename = u'%s/%s.%s' % (cfile['filepath'], cfile['filename'], cfile['ext']) meta_dict['hash'] = self.hashFile(filename) available_metadata = self.combineMetaData(available_metadata, meta_dict, vid_type=True) return self._getSecondarySourceMetadata(cfile, available_metadata) else: self._displayMessage(u"2-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile['filename'], cfile['inetref'])) return self._getSecondarySourceMetadata(cfile, available_metadata) # end _getTmdbMetadata def _getTvdbGraphics(self, cfile, graphic_type, toprated=False, watched=False): '''Download either a TV Series Poster, Banner, Fanart or Episode image return None return full qualified path and filename of downloaded graphic ''' rel_type = graphic_type if graphic_type == u'coverfile': graphic_type = u'poster' elif graphic_type == u'poster': rel_type =u'coverfile' self.config['g_defaultname']=False self.config['toprated'] = toprated self.config['nokeys'] = False self.config['maximum'] = u'1' if watched: self.config['sid']=cfile['inetref'] else: self.config['sid']=None self.config['episode_name'] = None self.config['series_name']=cfile['file_seriesname'] if not watched: self.config['season_num']=u"%d" % cfile['seasno'] self.config['episode_num']=u"%d" % cfile['epno'] # Special logic must be used if the (-MG) guessing option has been requested if not self.config['sid'] and self.config['mythtv_guess']: try: allmatchingseries = self.config['tvdb_api']._getSeries(self.config['series_name']) except Exception, e: self._displayMessage(u"tvdb Series not found(%s) or connection issues with thetvdb.com web site.\nError:(%s)\n" % (cfile['filename'], e)) return None if filter(is_not_punct_char, allmatchingseries['name'].lower()) == filter(is_not_punct_char,cfile['file_seriesname'].lower()): self.config['sid'] = allmatchingseries['sid'] self.config['series_name'] = allmatchingseries['name'] cfile['file_seriesname'] = allmatchingseries['name'] else: sys.stderr.write(u"\nGuessing could not find an exact match on tvdb for Series (%s)\ntherefore a graphics cannot be downloaded\n\n" % cfile['filename']) return None else: if not self.verifySeriesExists(): self._displayMessage(u"tvdb Series not found(%s)" % cfile['filename']) return None if watched: if self.program_seriesid == None: self.config['g_series'] = self.sanitiseFileName(cfile['file_seriesname'])+u' Season 1'+self.graphic_suffix[rel_type]+u'.%(ext)s' self.config['g_season'] = self.sanitiseFileName(cfile['file_seriesname'])+u' Season %(seasonnumber)d'+self.graphic_suffix[rel_type]+u'.%(ext)s' else: self.config['g_series'] = self.sanitiseFileName(self.program_seriesid)+self.graphic_suffix[rel_type]+u'.%(ext)s' self.config['g_season'] = self.sanitiseFileName(self.program_seriesid)+u' Season %(seasonnumber)d'+self.graphic_suffix[rel_type]+u'.%(ext)s' else: # TV Series ALWAYS need the ' Season' in the file name incase the show name could clobber a Movie image # Season X is used so that a real season image is not overritten. It will be renamed later. self.config['g_series'] = self.sanitiseFileName(self.config['series_name'])+u' Season X'+self.graphic_suffix[rel_type]+u'.%(ext)s' self.config['g_season'] = self.sanitiseFileName(self.config['series_name'])+u' Season %(seasonnumber)d'+self.graphic_suffix[rel_type]+u'.%(ext)s' if toprated: typegetGraphics=self.getTopRatedGraphics self.config['season_num']= None # Needed to get toprated graphics named in 'g_series' format else: typegetGraphics=self.getGraphics self.config['overwrite'] = True # Force overwriting any existing graphic file value = self._downloadGraphics(typegetGraphics(graphic_type), mythtv=True) self.config['overwrite'] = False # Turn off overwriting if value == None: return None else: return self.rtnRelativePath(value, graphicsDirectories[rel_type]) # end _getTvdbGraphics def _getTvdbMetadata(self, cfile, available_metadata): '''Download thetvdb.com meta data return what was input or results from a secondary source return dictionary of metadata ''' global video_type, UI_title video_type=u'TV series' UI_title = cfile['file_seriesname'] meta_dict={} self.config['nokeys'] = False self.config['sid']=None self.config['episode_name'] = None self.config['series_name']=cfile['file_seriesname'] self.config['season_num']=u"%d" % cfile['seasno'] self.config['episode_num']=u"%d" % cfile['epno'] if self.config['series_name_override']: if self.config['series_name_override'].has_key(cfile['file_seriesname'].lower()): self.config['sid'] = (self.config['series_name_override'][cfile['file_seriesname'].lower()]).strip() # Special logic must be used if the (-MG) guessing option has been requested if not self.config['sid'] and self.config['mythtv_guess']: try: allmatchingseries = self.config['tvdb_api']._getSeries(self.config['series_name']) except Exception, e: self._displayMessage(u"tvdb Series not found(%s) or there are connection problems with thetvdb.com\nError(%s)" % (cfile['filename'], e)) return None if filter(is_not_punct_char, allmatchingseries['name'].lower()) == filter(is_not_punct_char,cfile['file_seriesname'].lower()): self.config['sid'] = allmatchingseries['sid'] self.config['series_name'] = allmatchingseries['name'] cfile['file_seriesname'] = allmatchingseries['name'] else: sys.stderr.write(u"\nGuessing could not find an exact match on tvdb for Series (%s)\ntherefore a meta data dictionary cannot be returned\n\n" % cfile['filename']) return False else: if not self.verifySeriesExists(): self._displayMessage(u"tvdb Series not found(%s) meta data dictionary cannot be returned" % cfile['filename']) return self._getSecondarySourceMetadata(cfile, available_metadata) if self.config['sid'] == '99999999': if not self.config['interactive']: return self._getSecondarySourceMetadata(cfile, available_metadata) else: return {'sid': self.config['sid'], 'title': cfile['file_seriesname']} meta_dict={} tmp_array=(self.getSeriesEpisodeData()).split('\n') for element in tmp_array: element = (element.rstrip('\n')).strip() if element == '': continue index = element.index(':') key = element[:index].lower() data = element[index+1:] if data == None: continue if key == 'series': meta_dict['title'] = data continue if key == 'seasonnumber': try: meta_dict['season'] = int(data) except: pass continue if key == 'episodenumber': try: meta_dict['episode'] = int(data) except: pass continue if key == 'episodename': meta_dict['subtitle'] = data continue if key == u'overview': meta_dict['plot'] = data continue if key == u'director' and data == 'None': meta_dict['director'] = '' continue if key == u'firstaired' and len(data) > 4: try: meta_dict['year'] = int(data[:4]) except: pass meta_dict['firstaired'] = data try: meta_dict['releasedate'] = datetime.datetime.strptime(data,'%Y-%m-%d').date() except ValueError: pass continue if key == 'year': try: meta_dict['year'] = int(data) except: pass continue if key == 'seriesid': meta_dict['inetref'] = data meta_dict[key] = data continue if key == 'rating': try: meta_dict['userrating'] = float(data) except: pass continue if key == 'filename':# This "episodeimage URL clashed with the video file name and ep image continue # is not used yet. So skip fixes the db video filename from being wiped. if key == 'runtime': try: meta_dict['length'] = long(data) except: pass continue meta_dict[key] = data if len(meta_dict): if not meta_dict.has_key('director'): meta_dict['director'] = u'' meta_dict['rating'] = u'TV Show' # URL to TVDB web site episode web page for this series for url_data in [u'seriesid', u'seasonid', u'id']: if not url_data in meta_dict.keys(): break else: meta_dict['homepage'] = u'http://www.thetvdb.com/?tab=episode&seriesid=%s&seasonid=%s&id=%s' % (meta_dict['seriesid'], meta_dict['seasonid'], meta_dict['id']) if available_metadata['hash'] == u'' or available_metadata['hash'] == None: filename = u'%s/%s.%s' % (cfile['filepath'], cfile['filename'], cfile['ext']) meta_dict['hash'] = self.hashFile(filename) available_metadata = self.combineMetaData(available_metadata, meta_dict, vid_type=False) return self._getSecondarySourceMetadata(cfile, available_metadata) else: self._displayMessage(u"tvdb Series found (%s) but no meta data for dictionary" % cfile['filename']) return self._getSecondarySourceMetadata(cfile, available_metadata) # end _getTvdbMetadata def _make_db_ready(self, text): '''Prepare text for inclusion into a DB return None return data base ready text ''' if not text: return text try: text = text.replace(u'\u2013', u"-") text = text.replace(u'\u2014', u"-") text = text.replace(u'\u2018', u"'") text = text.replace(u'\u2019', u"'") text = text.replace(u'\u2026', u"...") text = text.replace(u'\u201c', u'"') text = text.replace(u'\u201d', u'"') except UnicodeDecodeError: pass return text # end make_db_ready def _addCastGenreCountry(self, data_string, vim, cast_genres_type): '''From a comma delimited string of cast members, genres or countries add the ones not already in the myth db and update the video's meta data return True when successfull return False if failed ''' if data_string == '': return True data = data_string.split(',') for i in range(len(data)): data[i]=data[i].strip() try: data.remove('') except: pass if cast_genres_type == 'genres': for item in data: vim.genre.add(item) elif cast_genres_type == 'cast': for item in data: vim.cast.add(item) elif cast_genres_type == 'countries': for item in data: vim.country.add(item) return True # end _addCastGenreCountry() # Local variables errors = [] new_names = [] def _moveDirectoryTree(self, src, dst, symlinks=False, ignore=None): '''Move a directory tree from a given source to a given destination. Subdirectories will be created and synbolic links will be recreated in the new destination. return an array of two arrays. Names of files/directories moved and Errors found ''' wild_card = False org_src = src if src[-1:] == '*': wild_card = True (src, fileName) = os.path.split(src) try: names = os.listdir(unicode(src, 'utf8')) except (UnicodeEncodeError, TypeError): names = os.listdir(src) else: if os.path.isfile(src): (src, fileName) = os.path.split(src) names = [fileName] else: try: names = os.listdir(unicode(src, 'utf8')) except (UnicodeEncodeError, TypeError): names = os.listdir(src) if ignore is not None: ignored_names = ignore(src, names) else: ignored_names = set() try: if self.config['simulation']: sys.stdout.write(u"Simulation creating subdirectories for file move (%s)\n" % dst) else: self._displayMessage(u"Creating subdirectories for file move (%s)\n" % dst) os.makedirs(dst) # Some of the subdirectories may already exist except OSError: pass for name in names: if name in ignored_names: continue srcname = os.path.join(src, name) dstname = os.path.join(dst, name) if not os.access(srcname, os.F_OK | os.R_OK | os.W_OK): # Skip any file that is not RW able sys.stderr.write(u"\n! Error: The Source video directory or file (%s) must have read and write permissions for be moved. File or directory has been skipped\n" % (srcname)) continue try: if symlinks and os.path.islink(srcname): linkto = os.readlink(srcname) if self.config['simulation']: sys.stdout.write(u"Simulation recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto, dstname)) else: os.symlink(linkto, dstname) self._displayMessage(u"Recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto, dstname)) self.num_symbolic_links+=1 elif os.path.isdir(srcname): if wild_card: self._displayMessage(u"Wildcard skipping subdirectory (%s)\n" % srcname) continue self.num_created_video_subdirectories+=1 self._displayMessage(u"Move subdirectory (%s)\n" % srcname) self._moveDirectoryTree(srcname, dstname, symlinks, ignore) else: if self.config['simulation']: if wild_card: if srcname.startswith(org_src[:-1]): sys.stdout.write(u"Simulation move wild card file from\n(%s) to\n(%s)\n" % (srcname, dstname)) self.num_moved_video_files+=1 self.new_names.append(dstname) else: self._displayMessage(u"Simulation of wildcard skipping file(%s)" % (srcname,)) else: sys.stdout.write(u"Simulation move file from\n(%s) to\n(%s)\n" % (srcname, dstname)) self.num_moved_video_files+=1 self.new_names.append(dstname) else: if wild_card: if srcname.startswith(org_src[:-1]): self._displayMessage(u"Move wild card file from\n(%s) to\n(%s)\n" % (srcname, dstname)) shutil.move(srcname, dstname) self.num_moved_video_files+=1 self.new_names.append(dstname) else: self._displayMessage(u"Wildcard skipping file(%s)" % (srcname,)) else: self._displayMessage(u"Move file from\n(%s) to\n(%s)\n" % (srcname, dstname)) shutil.move(srcname, dstname) self.num_moved_video_files+=1 self.new_names.append(dstname) # XXX What about devices, sockets etc.? except (IOError, os.error), why: self.errors.append([srcname, dstname, str(why)]) # catch the Error from the recursive move tree so that we can # continue with other files except: self.errors.append([src, dst, u"Unknown error"]) return [self.new_names, self.errors] # end _moveDirectoryTree # local variable for move stats num_moved_video_files=0 num_created_video_subdirectories=0 num_symbolic_links=0 def _moveVideoFiles(self, target_destination_array): """Copy files or directories to a destination directory. If the -F filename option is set then rename TV series during the move process. The move will be interactive for identifying a movie's IMDB number or TV series if the -i option was also set. If there is a problem error message are displayed and the script exists. After processing print a statistics report. return a array of video file dictionaries to update in Mythvideo data base """ global UI_selectedtitle # Validate that the targets and destinations actually exist. count=1 for file_dir in target_destination_array: if os.access(file_dir, os.F_OK | os.R_OK): if count % 2 == 0: # Destinations must all be directories if not os.path.isdir(file_dir): sys.stderr.write(u"\n! Error: Destinations must all be directories.\nThis destination is not a directory (%s)\n" % (file_dir,)) sys.exit(1) else: tmp_dir = file_dir for directory in self.config['mythvideo']: dummy_dir = file_dir.replace(directory, u'') if dummy_dir != tmp_dir: break else: sys.stderr.write(u"\n! Error: Destinations must all be a mythvideo directory or subdirectory.\nThis destination (%s) is not one of the Mythvideo directories(%s)\n" % (file_dir, self.config['mythvideo'], )) sys.exit(1) # Verify that a target file is really a video file. if file_dir[-1:] != '*': # Skip wildcard file name targets if os.access(file_dir, os.F_OK | os.R_OK): # Confirm that the file actually exists if not os.path.isdir(file_dir): ext = _getExtention(file_dir) for tmp_ext in self.config['video_file_exts']: if ext.lower() == tmp_ext: break else: sys.stderr.write(u"\n! Error: Target files must be video files(%s).\nSupported video file extentions(%s)\n" % (file_dir, self.config['video_file_exts'],)) sys.exit(1) count+=1 # Stats counters num_renamed_files = 0 num_mythdb_updates = 0 i = 0 video_files_to_process=[] cfile_array=[] while i < len(target_destination_array): src = target_destination_array[i] wild_card = False if src[-1:] == u'*': org_src = src wild_card = True (src, fileName) = os.path.split(src) dst = target_destination_array[i+1] self.errors = [] self.new_names = [] if wild_card: results = self._moveDirectoryTree(org_src, dst, symlinks=False, ignore=None) else: results = self._moveDirectoryTree(src, dst, symlinks=False, ignore=None) if len(results[1]): # Check if there are any errors sys.stderr.write(u"\n! Warning: There were errors during moving, with these directories/files\n") for error in results[1]: sys.stderr.write(u'\n! Warning: Source(%s), Destination(%s), Reason:(%s)\n' % (error[0], error[1], error[2])) tmp_cfile_array=[] for name in results[0]: file_name = os.path.join(dst, name) if os.path.isdir(file_name): for dictionary in self._processNames(_getFileList([file_name]), verbose = self.config['debug_enabled'], movies=True): tmp_cfile_array.append(dictionary) else: for dictionary in self._processNames([file_name], verbose = self.config['debug_enabled'], movies=True): tmp_cfile_array.append(dictionary) # Is the source directory within a mythvideo directory? If it is, # update existing mythdb records else add the record as you already have the inetref for directory in self.config['mythvideo']: if src.startswith(directory): for cfile in tmp_cfile_array: tmp_path = src+cfile['filepath'].replace(dst, u'') video_file = self.rtnRelativePath(self.movie_file_format % (tmp_path, cfile['filename'], cfile['ext']), 'mythvideo') tmp_filename = self.rtnRelativePath(self.movie_file_format % (cfile['filepath'], cfile['filename'], cfile['ext']), 'mythvideo') result = mythvideo.getVideo(exactfile=video_file) if result == None: intid = result else: intid = result.intid if not intid: result = mythvideo.getVideo(exactfile=self.movie_file_format % (tmp_path, cfile['filename'], cfile['ext']), host=localhostname.lower()) if result == None: intid = result else: intid = result.intid if intid: metadata = Video(id=intid, db=mythvideo) if tmp_filename[0] == '/': host = u'' self.absolutepath = True else: host = localhostname.lower() self.absolutepath = False if self.config['simulation']: sys.stdout.write(u"Simulation Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file, tmp_filename)) else: self._displayMessage(u"Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file, tmp_filename)) Video(id=intid, db=mythvideo).update({'filename': tmp_filename, 'host': host}) num_mythdb_updates+=1 break else: pass cfile_array.extend(tmp_cfile_array) i+=2 # Increment by 2 because array is int pairs of target and destination # Attempt to rename the video file if self.config['ret_filename']: for index in range(len(cfile_array)): cfile = cfile_array[index] if self.config['mythtv_inetref'] or self.config['mythtv_ref_num']: sys.stdout.write(u"\nAttempting to rename video filename (%s)\n" % cfile['file_seriesname']) if cfile['seasno'] == 0 and cfile['epno'] == 0: # File rename for a movie sid = None new_filename = u'' if self.config['series_name_override']: if self.config['series_name_override'].has_key(cfile['file_seriesname'].lower()): sid = self.config['series_name_override'][cfile['file_seriesname'].lower()] if not sid: data = self._getTmdbIMDB(cfile['file_seriesname'], rtnyear=True) if data: sid = data[u'sid'] if data[u'sid'] == '99999999': # The user chose to ignore this video continue new_filename = self.sanitiseFileName(data[u'name']) else: continue else: imdb_access = imdb.IMDb() try: data = imdb_access.get_movie(sid) if data.has_key('long imdb title'): new_filename = data['long imdb title'] elif data.has_key('title'): new_filename = self.sanitiseFileName(namedata['title']) else: continue except imdb._exceptions.IMDbDataAccessError: continue if not sid: # Cannot find this movie skip the renaming continue inetref = sid if not new_filename: continue else: cfile_array[index]['file_seriesname'] = new_filename else: # File rename for a TV Series Episode UI_selectedtitle = u'' new_filename = u'' self.config['sid'] = None self.config['series_name'] = cfile['file_seriesname'] if self.config['series_name_override']: if self.config['series_name_override'].has_key(cfile['file_seriesname'].lower()): self.config['sid'] = self.config['series_name_override'][cfile['file_seriesname'].lower()] self.config['series_name'] = None self.config['season_num'] = u"%d" % cfile['seasno'] self.config['episode_num'] = u"%d" % cfile['epno'] self.config['episode_name'] = None new_filename = self.returnFilename() inetref = self.config['sid'] if inetref == '99999999': # User chose to ignore this video continue if new_filename: if new_filename == cfile['filename']: # The file was already named to standard format self._displayMessage(u"File is already the correct name(%s)\n" % cfile['filename']) continue video_file = self.movie_file_format % (cfile['filepath'], cfile['filename'], cfile['ext']) tmp_filename = self.movie_file_format % (cfile['filepath'], new_filename, cfile['ext']) if self.config['simulation']: sys.stdout.write(u"Simulation file renamed from(%s) to(%s)\n" % (video_file, tmp_filename)) else: if not os.access(video_file, os.F_OK | os.R_OK | os.W_OK): sys.stdout.write(u"Cannot rename this file as it does not have read/write permissions set (%s)\n" % video_file) continue self._displayMessage(u"File renamed from(%s) to(%s)\n" % (video_file, tmp_filename)) os.rename(video_file, tmp_filename) num_renamed_files+=1 video_file = self.rtnRelativePath(self.movie_file_format % (cfile['filepath'], cfile['filename'], cfile['ext']), 'mythvideo') tmp_filename = self.rtnRelativePath(self.movie_file_format % (cfile['filepath'], new_filename, cfile['ext']), 'mythvideo') result = mythvideo.getVideo(exactfile=video_file) if result == None: intid = result else: intid = result.intid if not intid: result = mythvideo.getVideo(exactfile=self.movie_file_format % (cfile['filepath'], cfile['filename'], cfile['ext']), host=localhostname.lower()) if result == None: intid = result else: intid = result.intid if tmp_filename[0] == '/': host = u'' self.absolutepath = True else: host = localhostname.lower() self.absolutepath = False if intid: metadata = Video(id=intid, db=mythvideo) if self.config['simulation']: sys.stdout.write(u"Simulation Mythdb update for renamed file(%s)\n" % (tmp_filename)) else: self._displayMessage(u"Mythdb update for renamed file(%s)\n" % (tmp_filename)) Video(id=intid, db=mythvideo).update({'filename': tmp_filename, 'host': host}) else: if self.config['simulation']: sys.stdout.write(u"Simulation Mythdb add for renamed file(%s)\n" % (tmp_filename)) else: self._displayMessage(u"Adding Mythdb record for file(%s)\n" % (tmp_filename)) initrec = {} initrec[u'title'] = cfile['file_seriesname'] initrec[u'filename'] = tmp_filename initrec[u'host'] = host initrec[u'inetref'] = inetref Video(db=mythvideo).create(initrec) cfile_array[index]['filename'] = new_filename if self.config['simulation']: sys.stdout.write(u'\n---------Simulated Statistics---------------') sys.stdout.write('\n--------------Move Statistics---------------\nNumber of subdirectories ............(% 5d)\nNumber of files moved ...............(% 5d)\nNumber of symbolic links recreated...(% 5d)\nNumber of renamed TV-eps or movies.. (% 5d)\nNumber of Myth database updates .... (% 5d)\n--------------------------------------------\n\n' % (self.num_created_video_subdirectories, self.num_moved_video_files, self.num_symbolic_links, num_renamed_files, num_mythdb_updates)) return cfile_array # end _moveVideoFiles def _displayMessage(self, message): """Displays messages through stdout. Usually used with MythTv metadata updates in -V verbose mode. returns nothing """ if message[-1:] != '\n': message+='\n' if self.config['mythtv_verbose']: sys.stdout.write(message) # end _displayMessage def _findMissingInetref(self): '''Find any video file without a Mythdb record or without an inetref number return None if there are no new video files return a array of dictionary information on each video file that qualifies for processing ''' directories=self.config['mythvideo'] if not len(directories): sys.stderr.write(u"\n! Error: There must be a video directory specified in MythTv\n") sys.exit(1) allFiles = self._findFiles(directories, self.config['recursive'] , verbose = self.config['debug_enabled']) validFiles = self._processNames(allFiles, verbose = self.config['debug_enabled'], movies=True) if len(validFiles) == 0: # Is there video files to process? return None missing_list=[] for cfile in validFiles: try: videopath = self.movie_file_format % (cfile['filepath'], cfile['filename'], cfile['ext']) except UnicodeDecodeError: videopath = os.path.join(unicode(cfile['filepath'],'utf8'), unicode(cfile['filename'],'utf8')+u'.'+cfile['ext']) # Find the MythTV meta data result = mythvideo.getVideo(exactfile=videopath) if result == None: intid = result else: intid = result.intid if not intid: result = mythvideo.getVideo(exactfile=self.rtnRelativePath(videopath, 'mythvideo'), host=localhostname.lower()) if result == None: intid = result else: intid = result.intid if intid == None: missing_list.append(cfile) else: meta_dict = Video(id=intid, db=mythvideo) if self.config['video_dir']: if not mythvideo.getVideo(exactfile=meta_dict[u'filename'], host=meta_dict[u'host']): missing_list.append(cfile) continue # There must be an Internet reference number. Get one for new records. if _can_int(meta_dict['inetref']) and not meta_dict['inetref'] == u'00000000' and not meta_dict['inetref'] == '': continue missing_list.append(cfile) return missing_list # end _findMissingInetref def _checkValidGraphicFile(self, filename, graphicstype=u'', vidintid=False): '''Verify that a graphics file is not really an HTML file return True if it is a graphics file return False if it is an HTML file ''' # Verify that the graphics file is NOT HTML instead of the intended graphics file try: p = subprocess.Popen(u'file "%s"' % filename, shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) except: # There is something wrong with the file but do NOT say it is invalid just in case! return True data = p.stdout.readline() try: data = data.encode('utf8') except UnicodeDecodeError: data = unicode(data,'utf8') index = data.find(u'HTML document text') if index == -1: return True elif self.config['simulation']: sys.stdout.write( u"Simulation deleting bad graphics file (%s) as it is really HTML\n" % (filename, ) ) if vidintid: sys.stdout.write( u"and the MythVideo record was corrected for the graphic reference.\n" ) return False else: os.remove(filename) # Delete the useless HTML text sys.stderr.write( u"\n! Warning: The graphics file (%s) is actually HTML and not the intended file type.\nDuring the original file download the web site had issues. The bad downloaded file was removed.\n" % (filename)) if vidintid: repair = {} if graphicstype == u'coverfile': repair[graphicstype] = u'No Cover' else: repair[graphicstype] = u'' Video(id=vidintid, db=mythvideo).update(repair) return False # end _checkValidGraphicFile() def _graphicsCleanup(self): '''Match the graphics in the mythtv graphics directories with the ones specified by the mythvideometa records. Remove any graphics that are not referenced at least once. Print a report. ''' global localhostname num_total = 0 num_deleted = 1 num_new_total = 2 stats = {'coverfile': [0,0,0], 'banner': [0,0,0], 'fanart': [0,0,0]} graphics_file_dict={} all_graphics_file_list=[] for directory in graphicsDirectories.keys(): if directory == 'screenshot': continue file_list = _getFileList(self.config[graphicsDirectories[directory]]) if not len(file_list): graphics_file_dict[directory] = [] continue for g_file in list(file_list): # Cull the list removing dirs and non-graphics files if os.path.isdir(g_file): file_list.remove(g_file) continue g_ext = _getExtention(g_file) if not g_ext in self.image_extensions: file_list.remove(g_file) continue for filel in file_list: if not filel in all_graphics_file_list: all_graphics_file_list.append(filel) graphics_file_dict[directory] = file_list for key in graphicsDirectories.keys(): # Set initial totals if key == 'screenshot': continue stats[key][num_total] = len(graphics_file_dict[key]) # Start reading videometadata records to remove their graphics from the image orphan list try: records = mythvideo.searchVideos() except MythError, e: sys.stderr.write(u"\n! Error: Reading all videometadata records: %s\n" % e.args[0]) return atleast_one_video_file = False if records: for record in records: atleast_one_video_file = True meta_dict = {'host': record.host, 'coverfile': record.coverfile, 'banner': record.banner, 'fanart': record.fanart, 'filename': record.filename, 'intid': record.intid, 'inetref': record.inetref, } # Skip any videometadata record that is not for this host if meta_dict['host'] != u'' and meta_dict['host'] != None: if meta_dict['host'].lower() != localhostname.lower(): continue # Start removing any graphics in this videometadata record for key in meta_dict.keys(): if key in ['host','filename','intid', 'inetref']: continue if meta_dict[key] in [None, u'', u'None', u'No Cover', u'Unknown']: continue # Deal with videometadata record using storage groups if meta_dict['filename'] != None: if meta_dict['filename'][0] == u'/': self.absolutepath = True else: self.absolutepath = False if meta_dict[key][0] != '/': meta_dict[key] = self.rtnAbsolutePath(meta_dict[key], graphicsDirectories[key]) if meta_dict[key][0] != '/': # There is not a storage group for this relative file name continue # Deal with TV series level graphics (dirName, fileName) = os.path.split(meta_dict[key]) (fileBaseName, fileExtension)=os.path.splitext(fileName) index = fileBaseName.find(u' Season ') intid = meta_dict['intid'] if index != -1: # Is this a TV Series episode? if meta_dict[key] in graphics_file_dict[key]: if self._checkValidGraphicFile(meta_dict[key], graphicstype=key, vidintid=intid) == True: graphics_file_dict[key].remove(meta_dict[key]) all_graphics_file_list.remove(meta_dict[key]) # This logic is specific to Movies and videos with a '99999999' inetref numbers elif fileName.startswith(meta_dict['inetref']+u'_') or fileName.startswith(meta_dict['inetref']+u'.') or meta_dict['inetref'] == '99999999': if meta_dict[key] in graphics_file_dict[key]: if self._checkValidGraphicFile(meta_dict[key], graphicstype=key, vidintid=intid) == True: graphics_file_dict[key].remove(meta_dict[key]) all_graphics_file_list.remove(meta_dict[key]) if not atleast_one_video_file: sys.stderr.write(u"\n! Error: Janitor - did not find any video files to process so skipping\nimage clean up to protect your image files, in case this is a configuration or NFS error.\nIf you do not use MythVideo then the Janitor option (-MJ) is not of value to you on this MythTV back end.\n") return # end reading videometadata records to remove their graphics from the image orphan list # Get Scheduled and Recorded program list programs = self._getScheduledRecordedProgramList() # Remove Scheduled and Recorded program's graphics files from the delete list if programs: for field in graphicsDirectories.keys(): if field == 'screenshot': continue remove=[] for graphic in graphics_file_dict[field]: (dirName, fileName) = os.path.split(graphic) (fileBaseName, fileExtension)=os.path.splitext(fileName) for program in programs: if fileBaseName.lower().startswith(program['title'].lower()+u' '): remove.append(graphic) break if not isValidPosixFilename(program['title']) and program['seriesid'] != u'': if fileBaseName.lower().startswith(program['seriesid'].lower()): remove.append(graphic) break for rem in remove: if self._checkValidGraphicFile(rem, graphicstype=u'', vidintid=False) == True: graphics_file_dict[field].remove(rem) try: all_graphics_file_list.remove(rem) except ValueError, e: pass # Do not remove the MiroBridge default image files even if they are not currently being used for filel in list(all_graphics_file_list): if filel.endswith('mirobridge_coverart.jpg'): all_graphics_file_list.remove(filel) continue if filel.endswith('mirobridge_banner.jpg'): all_graphics_file_list.remove(filel) continue if filel.endswith('mirobridge_fanart.jpg'): all_graphics_file_list.remove(filel) continue for key in graphicsDirectories.keys(): # Set deleted files totals if key == 'screenshot': continue file_list = list(graphics_file_dict[key]) for filel in file_list: if not filel in all_graphics_file_list: graphics_file_dict[key].remove(filel) stats[key][num_deleted] = len(graphics_file_dict[key]) # Delete all graphics files still on the delete list for filel in all_graphics_file_list: if self.config['simulation']: sys.stdout.write( u"Simulation deleting (%s)\n" % (filel) ) else: try: os.remove(filel) except OSError: pass self._displayMessage(u"(%s) Has been deleted\n" % (filel)) for key in graphicsDirectories.keys(): # Set new files totals if key == 'screenshot': continue stats[key][num_new_total] = stats[key][num_total] - stats[key][num_deleted] if self.config['simulation']: sys.stdout.write(u'\n\n------------Simulated Statistics---------------') sys.stdout.write(u'\n--------------Janitor Statistics---------------\n') stat_type = ['total', 'deleted', 'new total'] for index in range(len(stat_type)): for key in graphicsDirectories.keys(): # Print stats if key == 'screenshot': continue if key == 'coverfile': g_type = 'posters' else: g_type = key+'s' sys.stdout.write(u'% 9s % 7s ......................(% 5d)\n' % (stat_type[index], g_type, stats[key][index], )) for key in graphicsDirectories.keys(): # Print stats if key == 'screenshot': continue if not len(graphics_file_dict[key]): continue if key == 'coverfile': g_type = 'poster' else: g_type = key sys.stdout.write(u'\n----------------Deleted %s files---------------\n' % g_type) for graphic in graphics_file_dict[key]: sys.stdout.write('%s\n' % graphic) return # end _graphicsCleanup def _getVideoLength(self, videofilename): '''Using ffmpeg (if it can be found) get the duration of the video return False if either ffmpeg cannot be found or the file is not a video return video lenght in minutes ''' if not self.config['ffmpeg']: return False # Filter out specific file types due to potential negative processing overhead if _getExtention(videofilename) in [u'iso', u'img', u'VIDEO_TS', u'm2ts', u'vob']: return False p = subprocess.Popen(u'ffmpeg -i "%s"' % (videofilename), shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) ffmpeg_found = True while 1: data = p.stderr.readline() if data.endswith('not found\n'): ffmpeg_found = False break if data.startswith(' Duration:'): break if data == '' and p.poll() != None: break if ffmpeg_found == False: self.config['ffmpeg'] = False return False elif data: time = (data[data.index(':')+1: data.index('.')]).strip() return (60*(int(time[:2]))+(int(time[3:5]))) else: return False # end _getVideoLength def _getMiroVideometadataRecords(self): """Fetches all videometadata records with an inetref of '99999999' and a category of 'Miro'. If the videometadata record has a host them it must match the lower-case of the locahostname. aborts if processing failed return and array of matching videometadata dictionary records """ global localhostname intids = [] try: records = mythvideo.searchVideos(category=u'Miro', custom=(('inetref=%s','99999999'),)) except MythError, e: sys.stderr.write(u"\n! Error: Reading all Miro videometadata records: %s\n" % e.args[0]) sys.exit(1) if records: for record in records: intids.append(record.intid) videometadatarecords=[] if len(intids): for intid in intids: vidrec = Video(id=intid, db=mythvideo) if vidrec[u'host'] != u'' and vidrec[u'host'] != None: if vidrec[u'host'].lower() != localhostname.lower(): continue videometadatarecords.append(vidrec) return videometadatarecords else: return None # end _getMiroVideometadataRecords() def _getExtraMiroDetails(self, mythvideorec, vidtype): '''Find the extra details required for Miro MythVideo record processing return a dictionary of details required for processing ''' extradata = {} extradata[u'intid'] = [mythvideorec[u'intid']] if vidtype == u'movies': extradata[u'tv'] = False else: extradata[u'tv'] = True for key in [u'coverfile', u'banner', u'fanart', ]: extradata[key] = True # Set each graphics type as if it has already been downloaded if mythvideorec[key] == None or mythvideorec[key] == u'No Cover' or mythvideorec[key] == u'': extradata[key] = False continue elif key == u'coverfile': # Look for undersized coverart if mythvideorec[u'filename'][0] == u'/': self.absolutepath = True else: self.absolutepath = False filename = self.rtnAbsolutePath(mythvideorec[key], graphicsDirectories[key]) try: (width, height) = self.config['image_library'].open(filename).size if width < self.config['min_poster_size']: extradata[key] = False continue except IOError: extradata[key] = False continue continue else: # Check if the default graphics are being used if mythvideorec[key].endswith(u'mirobridge_banner.jpg'): extradata[key] = False if mythvideorec[key].endswith(u'mirobridge_fanart.jpg'): extradata[key] = False continue if vidtype == u'movies': # Data specific to Movie Trailers if mythvideorec[u'filename'][0] == u'/': self.absolutepath = True else: self.absolutepath = False extradata[u'filename'] = mythvideorec[u'filename'] extradata[u'pathfilename'] = self.rtnAbsolutePath(mythvideorec[u'filename'], u'mythvideo') if os.path.islink(extradata[u'pathfilename']): extradata[u'symlink'] = True else: extradata[u'symlink'] = False moviename = mythvideorec['subtitle'] if not moviename: moviename = '' else: index = moviename.find(self.config[u'mb_movies'][filter(is_not_punct_char, mythvideorec[u'title'].lower())]) if index != -1: moviename = moviename[:index].strip() extradata[u'moviename'] = moviename extradata[u'inetref'] = False if not moviename == None and not moviename == '': lastyear = int(datetime.datetime.now().strftime(u"%Y")) years = [] i = 0 while i < 5: # Check for a Movie that will be released this year or the next four years years.append(u"%d" % ((lastyear+i))) i+=1 imdb_access = imdb.IMDb() movies_found = [] try: movies_found = imdb_access.search_movie(moviename.encode("ascii", 'ignore')) except Exception: pass tmp_movies={} for movie in movies_found: # Get rid of duplicates if movie.has_key('year'): temp = {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])} if tmp_movies.has_key(temp.keys()[0]): continue tmp_movies[temp.keys()[0]] = temp[temp.keys()[0]] for year in years: for movie in tmp_movies: if filter(is_not_punct_char, tmp_movies[movie][:-7].lower()) == filter(is_not_punct_char, moviename.lower()) and tmp_movies[movie][-5:-1] == year: extradata[u'inetref'] = u"%07d" % int(movie) extradata[u'moviename'] = tmp_movies[movie] extradata[u'year'] = year break if extradata[u'inetref']: break return extradata # end _getExtraMiroDetails() def updateMiroVideo(self, program): '''Update the information in a Miro/MythVideo record return nothing ''' global localhostname, graphicsDirectories mirodetails = program[u'miro'] for intid in mirodetails[u'intid']: changed_fields = {} for key in graphicsDirectories.keys(): if key == u'screenshot': continue if mirodetails[key] != True and mirodetails[key] != False and mirodetails[key] != None and mirodetails[key] != u'Simulated Secondary Source graphic filename place holder': # A graphics was downloaded changed_fields[key] = mirodetails[key] if not mirodetails[u'tv'] and not mirodetails[u'symlink'] and os.access(mirodetails[u'pathfilename'], os.F_OK | os.R_OK | os.W_OK): changed_fields[u'inetref'] = mirodetails[u'inetref'] changed_fields[u'subtitle'] = u'' changed_fields[u'year'] = mirodetails[u'year'] changed_fields[u'banner'] = u'' (dirName, fileName) = os.path.split(mirodetails[u'pathfilename']) (fileBaseName, fileExtension) = os.path.splitext(fileName) try: dir_list = os.listdir(unicode(dirName, 'utf8')) except (UnicodeEncodeError, TypeError): dir_list = os.listdir(dirName) index = 1 while index != 0: filename = self.sanitiseFileName(u'%s - Trailer %d' % (mirodetails[u'moviename'], index)) fullfilename = u'%s/%s%s' % (dirName, filename, fileExtension) for flenme in dir_list: if fnmatch.fnmatch(flenme.lower(), u'%s.*' % filename.lower()): break else: changed_fields[u'title'] = filename if self.config['simulation']: sys.stdout.write( u"Simulation rename Miro-MythTV movie trailer from (%s) to (%s)\n" % (mirodetails[u'pathfilename'], fullfilename)) else: os.rename(mirodetails[u'pathfilename'], fullfilename) changed_fields[u'filename'] = self.rtnRelativePath(fullfilename, u'mythvideo') if changed_fields[u'filename'][0] != u'/': changed_fields[u'host'] = localhostname.lower() else: # Deal with the whole mixing Video SG and local with SG graphics mess for key in graphicsDirectories.keys(): if key == u'screenshot' or not changed_fields.has_key(key): continue if changed_fields[key][0] == u'/': continue else: changed_fields.remove(key) break index+=1 if len(changed_fields): if self.config['simulation']: if program['subtitle']: sys.stdout.write( u"Simulation MythTV DB update for Miro video (%s - %s)\n" % (program['title'], program['subtitle'])) else: sys.stdout.write( u"Simulation MythTV DB update for Miro video (%s)\n" % (program['title'],)) else: Video(id=intid, db=mythvideo).update(changed_fields) # end updateMiroVideo() def _getScheduledRecordedProgramList(self): '''Find all Scheduled and Recorded programs return array of found programs, if none then empty array is returned ''' programs=[] # Get pending recordings try: progs = MythBE(backend=mythbeconn.hostname, db=mythbeconn.db).getUpcomingRecordings() except MythError, e: sys.stderr.write(u"\n! Error: Getting Upcoming Recordings list: %s\n" % e.args[0]) return programs for prog in progs: record={} if prog.title == None: continue record['title'] = prog.title record['subtitle'] = prog.subtitle record['seriesid'] = prog.seriesid if record['subtitle'] and prog.airdate != None: record['originalairdate'] = prog.airdate[:4] else: if prog.year != '0': record['originalairdate'] = prog.year elif prog.airdate != None: record['originalairdate'] = prog.airdate[:4] for program in programs: # Skip duplicates if program['title'] == record['title']: break else: programs.append(record) # Get recorded table field names: try: recordedlist = MythBE(backend=mythbeconn.hostname, db=mythbeconn.db).getRecordings() except MythError, e: sys.stderr.write(u"\n! Error: Getting recorded programs list: %s\n" % e.args[0]) return programs if not recordedlist: return programs recordedprogram = {} for recordedProgram in recordedlist: try: recordedRecord = recordedProgram.getRecorded() except MythError, e: sys.stderr.write(u"\n! Error: Getting recorded table record: %s\n" % e.args[0]) return programs if recordedRecord.recgroup == u'Deleted': continue recorded = {} if recordedRecord.title == None: continue if recordedRecord.chanid == 9999: recorded[u'miro_tv'] = True recorded[u'title'] = recordedRecord.title recorded[u'subtitle'] = recordedRecord.subtitle recorded[u'seriesid'] = recordedRecord.seriesid for program in programs: # Skip duplicates if program['title'] == recorded['title']: break else: programs.append(recorded) # Get Release year for recorded movies # Get Recorded videos recordedprogram / airdate try: recordedDetails = recordedRecord.getRecordedProgram() except MythError, e: sys.stderr.write(u"\n! Error: Getting recordedprogram table record: %s\n" % e.args[0]) continue if not recordedDetails: continue if not recordedDetails.subtitle: recordedprogram[recordedDetails.title]= u'%d' % recordedDetails.airdate # Add release year to recorded movies for program in programs: if recordedprogram.has_key(program['title']): program['originalairdate'] = recordedprogram[program['title']] # Add real names to mb_tv if they are among the recorded videos if len(self.config['mb_tv_channels']): for program in programs: programtitle = filter(is_not_punct_char, program[u'title'].lower()) if programtitle in self.config['mb_tv_channels'].keys(): self.config['mb_tv_channels'][programtitle][1] = program[u'title'] # Check that each program has an original airdate for program in programs: if not program.has_key('originalairdate'): program['originalairdate'] = u'0000' # Set the original airdate to zero (unknown) # If there are any Miro TV or movies to process then add them to the list if len(self.config['mb_tv_channels']) or len(self.config['mb_movies']): miromythvideorecs = self._getMiroVideometadataRecords() if miromythvideorecs: # Create array used to check for duplicates duplicatekeys = {} i = 0 for program in programs: programtitle = filter(is_not_punct_char, program[u'title'].lower()) if programtitle in self.config['mb_tv_channels'].keys(): if not program[u'title'] in duplicatekeys: duplicatekeys[program[u'title']] = i elif programtitle in self.config['mb_movies'].keys(): moviename = program['subtitle'] if not moviename: moviename = '' else: index = moviename.find(self.config['mb_movies'][programtitle]) if index != -1: moviename = moviename[:index].strip() if not moviename in duplicatekeys: duplicatekeys[moviename] = i i+=1 for record in miromythvideorecs: program = {} program[u'title'] = record[u'title'] program[u'subtitle'] = record[u'subtitle'] program[u'originalairdate'] = record[u'year'] recordtitle = filter(is_not_punct_char, record[u'title'].lower()) if recordtitle in self.config['mb_tv_channels'].keys(): if not record[u'title'] in duplicatekeys.keys(): program[u'miro'] = self._getExtraMiroDetails(record, u'tv') duplicatekeys[program[u'title']] = len(programs) programs.append(program) self.config['mb_tv_channels'][recordtitle][1] = record[u'title'] elif programs[duplicatekeys[program[u'title']]].has_key(u'miro'): programs[duplicatekeys[program[u'title']]][u'miro'][u'intid'].append(record[u'intid']) else: programs[duplicatekeys[program[u'title']]][u'miro'] = self._getExtraMiroDetails(record, u'tv') elif recordtitle in self.config['mb_movies'].keys(): moviename = record['subtitle'] if not moviename: moviename = '' else: index = moviename.find(self.config['mb_movies'][filter(is_not_punct_char, program[u'title'].lower())]) if index != -1: moviename = moviename[:index].strip() if not moviename in duplicatekeys.keys(): program[u'miro'] = self._getExtraMiroDetails(record, u'movies') if program[u'miro'][u'inetref']: duplicatekeys[moviename] = len(programs) programs.append(program) elif programs[duplicatekeys[moviename]].has_key(u'miro'): programs[duplicatekeys[moviename]][u'miro'][u'intid'].append(record[u'intid']) else: program[u'miro'] = self._getExtraMiroDetails(record, u'movies') if program[u'miro'][u'inetref']: programs[duplicatekeys[moviename]][u'miro'] = self._getExtraMiroDetails(record, u'movies') # Check that each program has seriesid for program in programs: if not program.has_key('seriesid'): program['seriesid'] = u'' # Set an empty seriesid - Generall only for Miro Videos if program['seriesid'] == None: program['seriesid'] = u'' # Set an empty seriesid return programs # end _getScheduledRecordedProgramList def _getScheduledRecordedTVGraphics(self, program, graphics_type): '''Get TV show graphics for Scheduled and Recorded TV programs return None if no graphics found return fullpath and filename of downloaded graphics file ''' if graphics_type == 'coverfile': graphics_type = 'poster' self.config['sid'] = None if self.config['series_name_override']: if self.config['series_name_override'].has_key(program['title'].lower()): self.config['sid'] = self.config['series_name_override'][program['title'].lower()] # Find out if there are any Series level graphics available self.config['toprated'] = True self.config['episode_name'] = None self.config['series_name'] = program['title'] self.config['season_num'] = None self.config['episode_num'] = None series_graphics = self.getGraphics(graphics_type) if series_graphics != None: cfile = { 'file_seriesname': program['title'], 'inetref': self.config['sid'], 'seasno': self.config['season_num'], 'epno': self.config['episode_num'], 'filepath':u'', 'filename': program['title'], 'ext':u'', 'categories':u'' } return self._getTvdbGraphics(cfile, graphics_type, toprated=True, watched=True) return None # end _getScheduledRecordedTVGraphics def _downloadScheduledRecordedGraphics(self): '''Get Scheduled and Recorded programs and Miro vidoes get their graphics if not already downloaded return (nothing is returned) ''' global localhostname # Initialize reporting stats total_progs_checked = 0 total_posters_found = 0 total_banners_found = 0 total_fanart_found = 0 total_posters_downloaded = 0 total_banners_downloaded = 0 total_fanart_downloaded = 0 total_miro_tv = 0 total_miro_movies = 0 programs = self._getScheduledRecordedProgramList() if not len(programs): # Is there any programs to process? return # Add any Miro Bridge mb_tv dictionary items to 'series_name_override' dictionary if not self.config['series_name_override'] and len(self.config['mb_tv_channels']): self.config['series_name_override'] = {} for miro_tv_key in self.config['mb_tv_channels'].keys(): if self.config['mb_tv_channels'][miro_tv_key][0]: self.config['series_name_override'][self.config['mb_tv_channels'][miro_tv_key][1].lower()] = self.config['mb_tv_channels'][miro_tv_key][0] total_progs_checked = len(programs) # Get totals of Miro TV shows and movies that will be processed for program in programs: if program.has_key(u'miro'): if not program[u'miro'][u'tv']: total_miro_movies+=1 else: total_miro_tv+=1 elif program.has_key(u'miro_tv'): if filter(is_not_punct_char, program[u'title'].lower()) in self.config['mb_movies'].keys(): total_miro_movies+=1 else: total_miro_tv+=1 # Prossess all TV shows and Movies for program in programs: program['need'] = False # Initalize that this program does not need graphic(s) downloaded mirodetails = None program_override_tv = False # Check if a subtitle-less program is really a TV show with an override. This compensates for # poor EPG data sources (as has been reported from at least Australia) if not program['subtitle'] and program['title'].lower() in self.config['series_name_override']: try: result = self._searchforSeries(program['title']) program_override_tv = True except Exception, e: pass # Even movies get the ' Season' added to the image names so that movie such as '1408' do not clash # with TMDB#ed image names pattern = u'%s Season*.*' if not program.has_key(u'miro'): if program['subtitle'] or program_override_tv: graphics_name = program['title'] else: if not int(program['originalairdate']): graphics_name = program['title'] else: graphics_name = "%s (%s)" % (program['title'], program['originalairdate']) else: mirodetails = program[u'miro'] if mirodetails[u'tv']: graphics_name = program['title'] else: graphics_name = mirodetails[u'inetref'] self.absolutepath = False # All Scheduled Recorded and Miro videos start in the SG "Default" # Search for graphics that are already downloaded for directory in graphicsDirectories.keys(): if directory == 'screenshot': # There is no downloading of screenshots required program[directory] = True continue if directory == 'banner' and not program['subtitle']: # No banners for movies program[directory] = True continue elif mirodetails: if not mirodetails[u'tv'] and directory == 'banner': # No banners for movies program[directory] = True continue if not mirodetails: filename = program['title'] elif mirodetails[u'tv']: filename = program['title'] else: filename = mirodetails[u'inetref'] # Deal with TV series names that would generate invalid file names for images TV and movies self.program_seriesid = None if not isValidPosixFilename(filename) and program['seriesid'] != u'': filename = program['seriesid'] self.program_seriesid = program['seriesid'] # Actual check for existing graphics for dirct in self.config[graphicsDirectories[directory]]: try: dir_list = os.listdir(unicode(dirct, 'utf8')) except (UnicodeEncodeError, TypeError): dir_list = os.listdir(dirct) for flenme in dir_list: if fnmatch.fnmatch(flenme.lower(), (pattern % filename).lower()): program[directory] = True if directory == 'coverfile': total_posters_found +=1 elif directory == 'banner': total_banners_found +=1 else: total_fanart_found +=1 if mirodetails: # Update the Miro MythVideo records with any existing graphics mirodetails[directory] = self.rtnRelativePath(u'%s/%s' % (dirct, flenme), directory) break else: continue break else: program['need'] = True program[directory] = False # Check if there are any graphics to download if not program['need']: if not mirodetails: filename = program['title'] elif mirodetails[u'tv']: filename = program['title'] else: filename = mirodetails[u'moviename'] self._displayMessage("All Graphics already downloaded for [%s]" % filename) if mirodetails: # Update the Miro MythVideo records with any new graphics self.updateMiroVideo(program) continue if not mirodetails: # It is more efficient to find inetref of movie once if not program['subtitle'] and not program_override_tv: if not program.has_key('inetref'): # Was the inetref number already found? inetref = self._getTmdbIMDB(graphics_name, watched=True) if not inetref: self._displayMessage("No movie inetref [%s]" % graphics_name) # Fake subtitle as this may be a TV series without a subtitle program['subtitle']=' ' else: self._displayMessage("Found movie inetref (%s),[%s]" % (inetref, graphics_name)) program['inetref'] = inetref elif not mirodetails[u'tv']: program['inetref'] = mirodetails[u'inetref'] # Download missing graphics for key in graphicsDirectories.keys(): if program[key]: # Check if this type of graphic is already downloaded continue miromovieflag = False if mirodetails: if not mirodetails[u'tv']: miromovieflag = True # This is a TV episode or Miro TV show if (program['subtitle'] or program_override_tv) and not miromovieflag: results = self._getScheduledRecordedTVGraphics(program, key) if results: if not mirodetails: filename = program['title'] elif mirodetails[u'tv']: filename = program['title'] else: filename = mirodetails[u'moviename'] if key == 'coverfile': total_posters_downloaded +=1 elif key == 'banner': total_banners_downloaded +=1 elif key == 'fanart': total_fanart_downloaded +=1 if mirodetails: # Save the filename for storing later mirodetails[key] = results else: self._displayMessage("TV Series - No (%s) for [%s]" % (key, program['title'])) else: # This is a movie title = program['title'] filename = program['title'] if miromovieflag: title = mirodetails[u'inetref'] filename = mirodetails[u'inetref'] cfile = { 'file_seriesname': title, 'inetref': program['inetref'], 'seasno': 0, 'epno': 0, 'filepath':u'', 'filename': filename, 'ext':u'', 'categories':u'' } if key == 'coverfile': g_type = '-P' else: g_type = '-B' results = self._getTmdbGraphics(cfile, g_type, watched=True) if not results: results = self._getSecondarySourceGraphics(cfile, key, watched=True) if results: if key == 'coverfile': total_posters_downloaded +=1 elif key == 'fanart': total_fanart_downloaded +=1 if mirodetails: # Save the filename for storing later mirodetails[key] = results else: if not mirodetails: filename = program['title'] else: filename = mirodetails[u'moviename'] self._displayMessage("No (%s) for [%s]" % (key, filename)) if mirodetails: # Update the Miro MythVideo records with any new graphics self.updateMiroVideo(program) # Print statistics sys.stdout.write(u'\n-----Scheduled & Recorded Statistics-------\nNumber of Scheduled & Recorded ......(% 5d)\nNumber of Fanart graphics found .....(% 5d)\nNumber of Poster graphics found .....(% 5d)\nNumber of Banner graphics found .....(% 5d)\nNumber of Fanart graphics downloaded (% 5d)\nNumber of Poster graphics downloaded (% 5d)\nNumber of Banner graphics downloaded (% 5d)\nNumber of Miro TV Shows ............ (% 5d)\nNumber of Miro Movie Trailers ...... (% 5d)\n' % (total_progs_checked, total_fanart_found, total_posters_found, total_banners_found, total_fanart_downloaded, total_posters_downloaded, total_banners_downloaded, total_miro_tv, total_miro_movies)) if len(programs): sys.stdout.write(u'\n-------------Scheduled & Recorded----------\n') for program in programs: if not program.has_key(u'miro'): if program.has_key(u'miro_tv'): if filter(is_not_punct_char, program[u'title'].lower()) in self.config['mb_movies'].keys(): sys.stdout.write(u'Miro Movie Trailer: %s\n' % (program['title'], )) else: sys.stdout.write(u'Miro TV Show: %s\n' % (program['title'], )) else: if program['subtitle']: sys.stdout.write(u'%s\n' % (program['title'], )) else: if program['originalairdate'] != u'0000': sys.stdout.write(u'%s\n' % ("%s (%s)" % (program['title'], program['originalairdate']))) else: sys.stdout.write(u'%s\n' % (program['title'], )) elif program[u'miro'][u'tv']: sys.stdout.write(u'Miro TV Show: %s\n' % (program['title'], )) else: sys.stdout.write(u'Miro Movie Trailer: %s\n' % (program[u'miro'][u'moviename'], )) return # end _downloadScheduledRecordedGraphics() def findFileInDir(self, filename, directories, suffix=None, fuzzy_match=False): '''Find if a file is in any of the specified directories. An exact match or a variation. return False - File not found in directories return True - Absolute file name and path ''' (dirName, fileName) = os.path.split(filename) (fileBaseName, fileExtension) = os.path.splitext(fileName) if fuzzy_match: # Match even when the names are not exactly the same by removing punctuation for dirct in directories: try: dir_list = os.listdir(unicode(dirct, 'utf8')) except (UnicodeEncodeError, TypeError): dir_list = os.listdir(dirct) match_list = [] for file_name in dir_list: match_list.append(filter(is_not_punct_char, file_name.lower())) if suffix: if fileBaseName.find(suffix) == -1: file_path = filter(is_not_punct_char, (u"%s%s%s" % (fileBaseName, suffix, fileExtension)).lower()) file_path2 = filter(is_not_punct_char, (u"%s%s" % (fileBaseName, fileExtension)).lower()) else: file_path = filter(is_not_punct_char, (u"%s%s" % (fileBaseName, fileExtension)).lower()) file_path2 = filter(is_not_punct_char, (u"%s%s" % (fileBaseName.replace(suffix, u''), fileExtension)).lower()) if file_path in match_list: return u'%s/%s' % (dirct, dir_list[match_list.index(file_path)]) if file_path2 in match_list: return u'%s/%s' % (dirct, dir_list[match_list.index(file_path2)]) continue else: file_path = filter(is_not_punct_char, (u"%s%s" % (fileBaseName, fileExtension)).lower()) if file_path in match_list: return u'%s/%s' % (dirct, dir_list[match_list.index(file_path)]) else: return False else: # Find an exact match for directory in directories: if filename[0] != u'/' and dirName != u'': dir_name = u"%s/%s" % (directory, dirName) else: dir_name = directory if suffix: if fileBaseName.find(suffix) == -1: file_path = u"%s/%s%s%s" % (dir_name, fileBaseName, suffix, fileExtension) file_path2 = u'%s/%s' % (dir_name, fileName) else: file_path = u'%s/%s' % (dir_name, fileName) file_path2 = u'%s/%s' % (dir_name, fileName.replace(suffix, u'')) if os.path.isfile(file_path): return file_path if os.path.isfile(file_path2): return file_path2 continue else: file_path = u'%s/%s' % (dir_name, fileName) if os.path.isfile(file_path): return file_path else: return False # end findFileInDir() # Local Variables num_secondary_source_graphics_downloaded=0 num_secondary_source_metadata_downloaded=0 def processMythTvMetaData(self): '''Check each video file in the mythvideo directories download graphics files and meta data then update MythTV data base meta data with any new information. ''' # Verify that the proper fields are present db_version = mythdb.settings.NULL.DBSchemaVer field_names = mythvideo.tablefields['videometadata'] for field in ['season', 'episode', 'coverfile', 'screenshot', 'banner', 'fanart']: if not field in field_names: sys.stderr.write(u"\n! Error: Your MythTv data base scheme version (%s) does not have the necessary fields at least (%s) is missing\n\n" % (db_version, field)) sys.exit(1) # Initailize and instance to the TMDB api apikey = "c27cb71cff5bd76e1a7a009380562c62" if self.config['interactive']: # themoviedb.org api key given by Travis Bell for Mythtv self.config['tmdb_api'] = tmdb_api.MovieDb(apikey, mythtv = True, interactive = True, select_first = False, debug = self.config['debug_enabled'], custom_ui = None, language = self.config['local_language'], search_all_languages = True,) else: self.config['tmdb_api'] = tmdb_api.MovieDb(apikey, mythtv = True, interactive = False, select_first = False, debug = self.config['debug_enabled'], language = self.config['local_language'], search_all_languages = True,) # If there were directories specified move them and update the MythTV db meta data accordingly if self.config['video_dir']: if len(self.config['video_dir']) % 2 == 0: validFiles = self._moveVideoFiles(self.config['video_dir']) self.config[u'file_move_flag'] = False else: sys.stderr.write(u"\n! Error: When specifying target (file or directory) to move to a destination (directory) they must always be in pairs (target and destination directory).\nYou specified an uneven number of variables (%d) for target and destination pairs.\nVariable count (%s)\n" % (len(self.config['video_dir']), self.config['video_dir'])) sys.exit(1) # Check if only missing inetref video's should be processed if self.config['mythtv_inetref'] or self.config['mythtv_ref_num']: validFiles = self._findMissingInetref() if validFiles == None: sys.stderr.write(u"\n! Warning: There were no missing interef video files found.\n\n") sys.exit(0) elif not len(validFiles): sys.stderr.write(u"\n! Warning: There were no missing interef video files found.\n\n") sys.exit(0) # Check if this is a Scheduled and Recorded graphics download request if self.config['mythtv_watched']: self._downloadScheduledRecordedGraphics() sys.exit(0) # Check if this is just a Janitor (clean up unused graphics files) request if self.config['mythtvjanitor']: self._graphicsCleanup() sys.exit(0) directories=self.config['mythvideo'] if not len(directories): sys.stderr.write(u"\n! Error: There must be a video directory specified in MythTv\n") sys.exit(1) # Set statistics num_processed=0 num_fanart_downloads=0 num_posters_downloads=0 num_banners_downloads=0 num_episode_metadata_downloads=0 num_movies_using_imdb_numbers=0 num_symlinks_created=0 num_mythdb_updates=0 num_posters_below_min_size=0 videos_with_small_posters=[] videos_using_imdb_numbers=[] videos_updated_metadata=[] missing_inetref=[] sys.stdout.write(u'Mythtv video database maintenance start: %s\n' % (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M")) if not self.config['video_dir'] and not self.config['mythtv_inetref'] and not self.config['mythtv_ref_num']: allFiles = self._findFiles(directories, self.config['recursive'] , verbose = self.config['debug_enabled']) validFiles = self._processNames(allFiles, verbose = self.config['debug_enabled'], movies=True) if not len(validFiles): sys.stderr.write(u"\n! Error: No valid video files found\n") sys.exit(1) tv_series_season_format=u"%s/%s Season %d.%s" tv_series_format=u"%s/%s.%s" for cfile in validFiles: self._displayMessage(u"\nNow processing video file (%s)(%s)(%s)\n" % (cfile['filename'], cfile['seasno'], cfile['epno'])) num_processed+=1 videopath = tv_series_format % (cfile['filepath'], cfile['filename'], cfile['ext']) # Find the MythTV meta data result = mythvideo.getVideo(exactfile=videopath) if result == None: intid = result else: intid = result.intid if not intid: result = mythvideo.getVideo(exactfile=self.rtnRelativePath(videopath, u'mythvideo'), host=localhostname.lower()) if result == None: intid = result has_metadata = False else: intid = result.intid if result.category == 'none' and result.year == 1895: has_metadata = False else: has_metadata = True else: if result.category == 'none' and result.year == 1895: has_metadata = False else: has_metadata = True if intid == None: # Unless explicitly requested with options -MI or -MG do not add missing videos to DB if not self.config['interactive'] and not self.config['mythtv_guess']: continue # Create a new empty entry sys.stdout.write(u"\n\nEntry does not exist in MythDB. Adding (%s).\n" % cfile['filename']) new_rec = {'title': cfile['file_seriesname'], 'filename': self.rtnRelativePath(videopath, u'mythvideo')} videopath = self.rtnRelativePath(videopath, u'mythvideo') if videopath[0] == '/': intid = Video(db=mythvideo).create(new_rec).intid else: new_rec['host'] = localhostname.lower() intid = Video(db=mythvideo).create(new_rec).intid elif not has_metadata: sys.stdout.write(u"\n\nEntry exists in MythDB but category is 0 and year is 1895 (default values).\nUpdating (%s).\n" % cfile['filename']) filename = self.rtnRelativePath(videopath, u'mythvideo') if filename[0] == u'/': Video(id=intid, db=mythvideo).update({'filename': filename, u'host': u''}) else: Video(id=intid, db=mythvideo).update({'filename': filename, u'host': localhostname.lower()}) if cfile['seasno'] == 0 and cfile['epno'] == 0: movie=True else: movie=False # Get a dictionary of the existing meta data plus a copy for update comparison meta_dict={} vim = Video(id=intid, db=mythvideo) for key in vim.keys(): meta_dict[key] = vim[key] # Fix a metadata record that has an incorrectly initialized inetref number value if meta_dict['inetref'] == None: meta_dict['inetref'] = u'00000000' available_metadata = dict(meta_dict) available_metadata['season']=cfile['seasno'] available_metadata['episode']=cfile['epno'] if available_metadata['title'] == u'': available_metadata['title'] = cfile['file_seriesname'] # Set whether a video file is stored in a Storage Group or not if available_metadata['filename'][0] == u'/': self.absolutepath = True else: self.absolutepath = False # There must be an Internet reference number. Get one for new records. if _can_int(meta_dict['inetref']) and not meta_dict['inetref'] == u'00000000' and not meta_dict['inetref'] == '': if meta_dict['inetref'] == '99999999': # Records that are not updated by Jamu continue inetref = meta_dict['inetref'] cfile['inetref'] = meta_dict['inetref'] else: if movie: if not self.config['interactive'] and not self.config['mythtv_guess']: sys.stderr.write(u'\n! Warning: Skipping "%s" as there is no TMDB or IMDB number for this movie.\nUse interactive option (-I) or (-R) to select the TMDB or IMDB number.\n\n' % (cfile['file_seriesname'])) continue inetref = self._getTmdbIMDB(available_metadata['title']) cfile['inetref'] = inetref if not inetref: self._displayMessage(u"themoviedb.com does not recognize the movie (%s) - Cannot update metadata - skipping\n" % available_metadata['title']) missing_inetref.append(available_metadata['title']) continue # Only update the reference number if self.config['mythtv_ref_num'] or inetref == '99999999': Video(id=intid, db=mythvideo).update({'inetref': inetref}) num_mythdb_updates+=1 videos_updated_metadata.append(cfile['filename']) self._displayMessage(u"\nReference number (%s) added for (%s) \n" % (inetref, cfile['filename'])) continue else: copy = {} for key in available_metadata.keys(): copy[key] = available_metadata[key] tmp_dict = self._getTvdbMetadata(cfile, copy) if not tmp_dict: self._displayMessage(u"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile['seasno'], cfile['epno'], videopath)) missing_inetref.append(available_metadata['title']) continue inetref = tmp_dict['inetref'] available_metadata['title'] = tmp_dict['title'] cfile['file_seriesname'] = tmp_dict['title'] # Only update the reference number and title if self.config['mythtv_ref_num'] or inetref == '99999999': if inetref == u'99999999': Video(id=intid, db=mythvideo).update({'inetref': inetref}) else: Video(id=intid, db=mythvideo).update({'inetref': inetref, 'title': tmp_dict['title']}) num_mythdb_updates+=1 videos_updated_metadata.append(cfile['filename']) self._displayMessage(u"\nReference number (%s) added for (%s) \n" % (inetref, cfile['filename'])) continue cfile['inetref'] = inetref available_metadata['inetref'] = inetref if (meta_dict['subtitle'] == None or meta_dict['subtitle'] == '') and not movie: tmp_subtitle = self._getSubtitle(cfile) if tmp_subtitle == None: self._displayMessage(u"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile['seasno'], cfile['epno'], videopath)) continue else: available_metadata['subtitle'] = tmp_subtitle available_metadata['title'] = self.config['series_name'] cfile['file_seriesname'] = self.config['series_name'] # Check if current inetref is a IMDB# # If so then check it can be changed to tmdb# # If it can be changed then rename any graphics and update meta data if movie and len(inetref) == 7: self._displayMessage(u"%s has IMDB# (%s)" % (available_metadata['title'], inetref)) num_movies_using_imdb_numbers+=1 videos_using_imdb_numbers.append(u"%s has IMDB# (%s)" % (available_metadata['title'], inetref)) movie_data = self._getTmdbMetadata(cfile, dict(available_metadata)) if movie_data.has_key('inetref'): if available_metadata['inetref'] != movie_data['inetref']: available_metadata['inetref'] = movie_data['inetref'] inetref = movie_data['inetref'] cfile['inetref'] = movie_data['inetref'] for graphic_type in ['coverfile', 'banner', 'fanart']: # Rename graphics files if available_metadata[graphic_type] == None or available_metadata[graphic_type] == '': continue graphic_file = self.rtnAbsolutePath(available_metadata[graphic_type], graphicsDirectories[graphic_type]) if os.path.isfile(graphic_file): filepath, filename = os.path.split(graphic_file) filename, ext = os.path.splitext( filename ) ext = ext[1:] if self.config['simulation']: sys.stdout.write( u"Simulation renaming (%s) to (%s)\n" % (graphic_file, tv_series_format % (filepath, inetref+self.graphic_suffix[graphic_type], ext)) ) else: dest = tv_series_format % (filepath, inetref+self.graphic_suffix[graphic_type], ext) try: if not os.path.isfile(dest): os.rename(graphic_file, dest) except IOError, e: sys.stderr.write( u"Renaming image file (%s) to (%s) failed, error(%s)\n" % (graphic_file, dest, e)) self._displayMessage(u"Renamed (%s) to (%s)\n" % (graphic_file, tv_series_format % (filepath, inetref+self.graphic_suffix[graphic_type], ext))) available_metadata[graphic_type]= self.rtnRelativePath(dest, graphicsDirectories[graphic_type]) ############################################################################### # START of metadata Graphics logic - Checking, downloading, renaming ############################################################################### for graphic_type in ['coverfile', 'banner', 'fanart']: ############################################################################### # START of MOVIE graphics updating ############################################################################### # Check that there are local graphics path for abs path video # An abs path video can only use the FE specified graphic directories if self.absolutepath: if not len(self.config['localpaths'][graphicsDirectories[graphic_type]]): continue graphicsdirs = self.config['localpaths'][graphicsDirectories[graphic_type]] else: graphicsdirs = self.config[graphicsDirectories[graphic_type]] if movie: if graphic_type == 'banner': continue if graphic_type == 'coverfile': g_type = '-P' else: g_type = '-B' need_graphic = True undersized_graphic = False for ext in self.image_extensions: for graphicsdir in graphicsdirs: filename = self.findFileInDir(u"%s.%s" % (inetref, ext), [graphicsdir], suffix=self.graphic_suffix[graphic_type]) if filename: available_metadata[graphic_type]=self.rtnRelativePath(filename, graphicsDirectories[graphic_type]) if graphic_type == 'coverfile': try: (width, height) = self.config['image_library'].open(filename).size if width < self.config['min_poster_size']: num_posters_below_min_size+=1 videos_with_small_posters.append(cfile['filename']) undersized_graphic = True break except IOError: undersized_graphic = True break need_graphic = False break if not need_graphic: break if need_graphic == True: dummy_graphic = self._getTmdbGraphics(cfile, g_type) # Try secondary source if themoviedb.com did not have graphicrecord['title'] if dummy_graphic == None or undersized_graphic == True: dummy_graphic = self._getSecondarySourceGraphics(cfile, graphic_type) if dummy_graphic != None: available_metadata[graphic_type] = self.rtnRelativePath(dummy_graphic, graphicsDirectories[graphic_type]) if graphic_type == 'fanart': self._displayMessage(u"Movie - Added fan art for(%s)" % cfile['filename']) num_fanart_downloads+=1 else: self._displayMessage(u"Movie - Added a poster for(%s)" % cfile['filename']) num_posters_downloads+=1 continue # END of Movie graphics updates ############################################### else: ############################################################################### # START of TV Series graphics updating ############################################################################### need_graphic = False new_format = True # Initialize that a graphics file NEEDS a new format # Check if an existing TV series graphic file is in the old naming format if available_metadata[graphic_type] != None and available_metadata[graphic_type] != 'No Cover' and available_metadata[graphic_type] != '': graphic_file = self.rtnAbsolutePath(available_metadata[graphic_type], graphicsDirectories[graphic_type]) filepath, filename = os.path.split(graphic_file) filename, ext = os.path.splitext( filename ) if filename.find(u' Season ') != -1: new_format = False else: need_graphic = True if need_graphic or new_format: # Graphic does not exist or is in an old format for ext in self.image_extensions: for graphicsdir in graphicsdirs: filename=self.findFileInDir(u"%s Season %d.%s" % (self.sanitiseFileName(available_metadata['title']), available_metadata['season'], ext), [graphicsdir], suffix=self.graphic_suffix[graphic_type], fuzzy_match=True) if filename: available_metadata[graphic_type]=self.rtnRelativePath(filename, graphicsDirectories[graphic_type]) need_graphic = False if graphic_type == 'coverfile': try: (width, height) = self.config['image_library'].open(filename).size if width < self.config['min_poster_size']: num_posters_below_min_size+=1 videos_with_small_posters.append(cfile['filename']) break except IOError: undersized_graphic = True break break if not need_graphic: break else: graphic_file = self.rtnAbsolutePath(available_metadata[graphic_type], graphicsDirectories[graphic_type]) if not graphic_file == None: graphic_file = self.findFileInDir(graphic_file, [graphicsdir], suffix=self.graphic_suffix[graphic_type], fuzzy_match=True) if graphic_file == None: need_graphic = True if not need_graphic: # Have graphic but may be using an old naming convention must_rename = False season_missing = False suffix_missing = False if graphic_file.find(u' Season ') == -1: # Check for Season must_rename = True season_missing = True if graphic_file.find(self.graphic_suffix[graphic_type]) == -1: must_rename = True suffix_missing = True if must_rename: filepath, filename = os.path.split(graphic_file) baseFilename, ext = os.path.splitext( filename ) baseFilename = self.sanitiseFileName(baseFilename) if season_missing and suffix_missing: newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename, available_metadata['season'], self.graphic_suffix[graphic_type], ext) elif suffix_missing: newFilename = u"%s/%s%s%s" % (filepath, baseFilename, self.graphic_suffix[graphic_type], ext) elif season_missing: baseFilename = baseFilename.replace(self.graphic_suffix[graphic_type], u'') newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename.replace(u' Season X', u''), available_metadata['season'], self.graphic_suffix[graphic_type], ext) if self.config['simulation']: sys.stdout.write( u"Simulation renaming (%s) to (%s)\n" % (graphic_file, newFilename) ) else: os.rename(graphic_file, newFilename) available_metadata[graphic_type]= self.rtnRelativePath(newFilename, graphicsDirectories[graphic_type]) else: available_metadata[graphic_type]= self.rtnRelativePath(graphic_file, graphicsDirectories[graphic_type]) else: # Must see if a graphic is on thetvdb wiki if graphic_type == 'coverfile' or graphic_type == 'banner': available_metadata[graphic_type] = self.rtnRelativePath(self._getTvdbGraphics(cfile, graphic_type), graphicsDirectories[graphic_type]) if available_metadata[graphic_type] == None: tmp = self._getTvdbGraphics(cfile, graphic_type, toprated=True) if tmp!= None: tmp_fullfilename = self.rtnAbsolutePath(tmp, graphicsDirectories[graphic_type]) filepath, filename = os.path.split(tmp_fullfilename) baseFilename, ext = os.path.splitext( filename ) baseFilename = self.sanitiseFileName(baseFilename) baseFilename = baseFilename.replace(self.graphic_suffix[graphic_type], u'') newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename.replace(u' Season X', u''), available_metadata['season'], self.graphic_suffix[graphic_type], ext) if self.config['simulation']: sys.stdout.write( u"Simulation rename (%s) to (%s)\n" % (tmp_fullfilename,newFilename) ) else: self._displayMessage(u"Rename existing graphic %s for series (%s)" % (graphic_type, available_metadata['title'])) try: os.rename(tmp_fullfilename, newFilename) if graphic_type == 'coverfile': self._displayMessage("1-Added a poster for(%s)" % cfile['filename']) num_posters_downloads+=1 else: self._displayMessage("1-Added a banner for(%s)" % cfile['filename']) num_banners_downloads+=1 available_metadata[graphic_type] = self.rtnRelativePath(newFilename, graphicsDirectories[graphic_type]) except IOError, e: sys.stderr.write( u"IOError coping (%s) to (%s)\nError:(%s)\n" % (tmp_fullfilename, newFilename, e)) else: # Try a secondary source dummy = self._getSecondarySourceGraphics(cfile, graphic_type) if dummy: if graphic_type == 'coverfile': self._displayMessage(u"1-Secondary source poster for(%s)" % cfile['filename']) num_posters_downloads+=1 else: self._displayMessage(u"1-Secondary source banner for(%s)" % cfile['filename']) num_banners_downloads+=1 available_metadata[graphic_type] = self.rtnRelativePath(dummy, graphicsDirectories[graphic_type]) else: # download fanart tmp = self.rtnAbsolutePath(self._getTvdbGraphics(cfile, graphic_type, toprated=True), graphicsDirectories['fanart']) if tmp!= None: filepath, filename = os.path.split(tmp) baseFilename, ext = os.path.splitext( filename ) baseFilename = self.sanitiseFileName(baseFilename) baseFilename = baseFilename.replace(self.graphic_suffix[graphic_type], u'') newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename.replace(u' Season X', u''), available_metadata['season'], self.graphic_suffix[graphic_type], ext) if self.config['simulation']: sys.stdout.write( u"Simulation fanart rename (%s) to (%s)\n" % (tmp, newFilename) ) else: try: os.rename(self.rtnAbsolutePath(tmp, graphicsDirectories[graphic_type]), newFilename) available_metadata['fanart'] = self.rtnRelativePath(newFilename, graphicsDirectories['fanart']) num_fanart_downloads+=1 except IOError, e: sys.stderr.write( u"IOError coping (%s) to (%s)\nError:(%s)\n" % (self.rtnAbsolutePath(tmp, graphicsDirectories[graphic_type]), newFilename, e)) else: # Try a secondary source dummy = self._getSecondarySourceGraphics(cfile, graphic_type) if dummy: available_metadata['fanart'] = self.rtnRelativePath(dummy, graphicsDirectories['fanart']) num_fanart_downloads+=1 # END of TV Series graphics updating ############################################################################### # END of metadata Graphics logic - Checking, downloading, renaming ############################################################################### ############################################################################### # START of metadata text logic - Checking, downloading, renaming ############################################################################### # Clean up meta data code if movie: if available_metadata['rating'] == 'TV Show': available_metadata['rating'] = 'NR' # Check if any meta data needs updating metadata_update = True for key in available_metadata.keys(): if key in self.config['metadata_exclude_as_update_trigger']: continue else: if key == 'rating' and (available_metadata[key] == 'NR' or available_metadata[key] == '' or available_metadata[key] == 'Unknown'): self._displayMessage( u"At least (%s) needs updating\n" % (key)) break if key == 'userrating' and available_metadata[key] == 0.0: self._displayMessage( u"At least (%s) needs updating\n" % (key)) break if key == 'length' and available_metadata[key] == 0: self._displayMessage( u"At least (%s) needs updating\n" % (key)) break if key == 'category' and available_metadata[key] == 0: self._displayMessage( u"At least (%s) needs updating\n" % (key)) break if key == 'year' and (available_metadata[key] == 0 or available_metadata[key] == 1895): self._displayMessage( u"At least (%s) needs updating\n" % (key)) break if movie and key == 'subtitle': # There are no subtitles in movies continue if movie and key == 'plot' and available_metadata[key] != None: if len(available_metadata[key].split(' ')) < 10: self._displayMessage( u"The plot is less than 10 words check if a better plot exists\n") break if key == 'releasedate' and (available_metadata[key] == None or available_metadata[key] == date(1,1,1)): self._displayMessage( u"At least (%s) needs updating\n" % (key)) break if key == 'hash' and (available_metadata['hash'] == u'' or available_metadata['hash'] == None): if (os.path.getsize(u'%s/%s.%s' % (cfile['filepath'], cfile['filename'], cfile['ext'])) < 65536 * 2): continue self._displayMessage( u"At least (%s) needs updating\n" % (key)) break if available_metadata[key] == None or available_metadata[key] == '' or available_metadata[key] == 'None' or available_metadata[key] == 'Unknown': self._displayMessage( u"At least (%s) needs updating\n" % (key)) break else: metadata_update = False if not movie and not len(available_metadata['inetref']) >= 5: self._displayMessage( u"At least (%s) needs updating\n" % ('inetref')) metadata_update = True # Find the video file's real duration in minutes try: length = self._getVideoLength(u'%s/%s.%s' % (cfile['filepath'], cfile['filename'], cfile['ext'], )) except: length = False if length: if length != available_metadata['length']: self._displayMessage(u"Video file real length (%d) minutes needs updating\n" % (length)) metadata_update = True # Fetch meta data genres_cast={'genres': u'', 'cast': u''} if metadata_update: copy = dict(available_metadata) if movie: tmp_dict = self._getTmdbMetadata(cfile, copy) else: tmp_dict = self._getTvdbMetadata(cfile, copy) num_episode_metadata_downloads+=1 # Update meta data if tmp_dict: for key in ['genres', 'cast', 'countries']: if tmp_dict.has_key(key): genres_cast[key] = tmp_dict[key] for key in available_metadata.keys(): if key in self.config['metadata_exclude_as_update_trigger']: continue else: if not tmp_dict.has_key(key): continue if key == 'userrating' and available_metadata[key] == 0.0: available_metadata[key] = tmp_dict[key] continue if key == 'length': try: length = self._getVideoLength(u'%s/%s.%s' %(cfile['filepath'], cfile['filename'], cfile['ext'], )) except: length = False if length: available_metadata['length'] = length else: available_metadata[key] = tmp_dict[key] continue available_metadata[key] = tmp_dict[key] # Fix fields that must be prepared for insertion into data base available_metadata['title'] = self._make_db_ready(available_metadata['title']) available_metadata['director'] = self._make_db_ready(available_metadata['director']) available_metadata['plot'] = self._make_db_ready(available_metadata['plot']) if available_metadata['year'] == 0: available_metadata['year'] = 1895 if available_metadata['coverfile'] == None: available_metadata['coverfile'] = u'No Cover' if len(genres_cast['genres']) and available_metadata['category'] == 'none': try: genres = genres_cast['genres'][:genres_cast['genres'].index(',')] except: genres = genres_cast['genres'] available_metadata['category'] = genres self._displayMessage(u"Category added for (%s)(%s)" % (available_metadata['title'], available_metadata['category'])) # Make sure graphics relative/absolute paths are set PROPERLY based # on the 'filename' field being a relative or absolute path. A filename with an absolite path # CAN ONLY have graphics baed on absolute paths. # A filename with a relative path can have mixed absolute and relative path graphic files if available_metadata[u'filename'][0] == u'/': available_metadata[u'host'] = u'' for key in [u'coverfile', u'banner', u'fanart']: if available_metadata[key] != None and available_metadata[key] != u'No Cover' and available_metadata[key] != u'': if available_metadata[key][0] != u'/': tmp = self.rtnAbsolutePath(available_metadata[key], graphicsDirectories[key]) if tmp[0] != u'/': if key == u'coverfile': available_metadata[key] = u'No Cover' else: available_metadata[key] = u'' else: available_metadata[u'host'] = localhostname.lower() ############################################################################### # END of metadata text logic - Checking, downloading, renaming ############################################################################### ############################################################################### # START of metadata updating the MythVideo record when graphics or text has changed ############################################################################### # Check if any new information was found if not self.config['overwrite']: for key in available_metadata.keys(): if available_metadata[key] != meta_dict[key]: if available_metadata[key] == u'' and meta_dict[key] == None: continue if available_metadata[key] == u'' and meta_dict[key] == u'Unknown': continue try: self._displayMessage( u"1-At least (%s)'s value(%s) has changed new(%s)(%s) old(%s)(%s)\n" % (cfile['filename'], key, available_metadata[key], type(available_metadata[key]), meta_dict[key], type(meta_dict[key]))) except: self._displayMessage( u"2-At least (%s)'s value(%s) has changed new(%s) old(%s)\n" % (cfile['filename'], key, type(available_metadata[key]), type(meta_dict[key]))) break else: self._displayMessage( u"Nothing to update for video file(%s)\n" % cfile['filename'] ) continue if self.config['simulation']: sys.stdout.write( u"Simulation MythTV DB update for video file(%s)\n" % cfile['filename'] ) for key in available_metadata.keys(): print key," ", available_metadata[key] for key in genres_cast.keys(): sys.stdout.write(u"Key(%s):(%s)\n" % (key, genres_cast[key])) else: sys.stdout.write('\n') else: # Clean up a few fields before updating Mythdb if available_metadata['showlevel'] == 0: # Allows mythvideo to display this video available_metadata['showlevel'] = 1 Video(id=intid, db=mythvideo).update(available_metadata) num_mythdb_updates+=1 videos_updated_metadata.append(cfile['filename']) for key in ['genres', 'cast', 'countries']: if key == 'genres' and len(cfile['categories']): genres_cast[key]+=cfile['categories'] if genres_cast.has_key(key): self._addCastGenreCountry( genres_cast[key], Video(id=intid, db=mythvideo), key) self._displayMessage( u"Updated Mythdb for video file(%s)\n" % cfile['filename'] ) ############################################################################### # END of metadata updating the MythVideo record when graphics or text has changed ############################################################################### sys.stdout.write(u"\nMythtv video database maintenance ends at : %s\n" % (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M")) # Print statistics sys.stdout.write(u'\n------------------Statistics---------------\nNumber of video files processed .....(% 5d)\nNumber of Fanart graphics downloaded (% 5d)\nNumber of Poster graphics downloaded (% 5d)\nNumber of Banner graphics downloaded (% 5d)\nNumber of 2nd source graphics downld (% 5d)\nNumber of metadata downloads.........(% 5d)\nNumber of 2nd source metadata found .(% 5d)\nNumber of symbolic links created.....(% 5d)\nNumber of Myth database updates......(% 5d)\nNumber of undersized posters ........(% 5d)\nNumber of Movies using IMDB numbers .(% 5d)\n' % (num_processed, num_fanart_downloads, num_posters_downloads, num_banners_downloads, self.num_secondary_source_graphics_downloaded, num_episode_metadata_downloads, self.num_secondary_source_metadata_downloaded, num_symlinks_created, num_mythdb_updates, num_posters_below_min_size, num_movies_using_imdb_numbers)) if len(videos_updated_metadata): sys.stdout.write(u'\n--------------Updated Video Files----------\n' ) for videofile in videos_updated_metadata: sys.stdout.write(u'%s\n' % videofile) if len(missing_inetref): sys.stdout.write(u'\n----------------No Inetref Found-----------\n' ) for videofile in missing_inetref: sys.stdout.write(u'%s\n' % videofile) if len(videos_with_small_posters): sys.stdout.write(u'\n---------------Under sized Poster----------\n' ) for videofile in videos_with_small_posters: sys.stdout.write(u'%s\n' % videofile) if len(videos_using_imdb_numbers): sys.stdout.write(u'\n---------------Movies with IMDB#s----------\n' ) for videofile in videos_using_imdb_numbers: sys.stdout.write(u'%s\n' % videofile) return None # end processMythTvMetaData def __repr__(self): # Just a place holder return self.config # end __repr__ # end MythTvMetaData def simple_example(): """Simple example of using jamu Displays the poster graphics URL(s) and episode meta data for the TV series Sanctuary, season 1 episode 3 returns None if there was no data found for the request TV series returns False if there is no TV series as specified returns a string with poster URLs and episode meta data """ # Get an instance of the variable configuration information set to default values configuration = Configuration(interactive = True, debug = False) # Set the type of data to be returned configuration.changeVariable('get_poster', True) configuration.changeVariable('get_ep_meta', True) # Validate specific variables and set the TV series information configuration.validate_setVariables(['Sanctuary', '1', '3']) # Get an instance of the tvdb process function and fetch the data process = Tvdatabase(configuration.config) results = process.processTVdatabaseRequests() if results != None and results != False: # Print the returned data string to the stdout print process.processTVdatabaseRequests().encode('utf8') # end simple_example def main(): """Support jamu from the command line returns True """ parser = OptionParser(usage=u"%prog usage: jamu -hbueviflstdnmoCRFUDSGN [parameters]\n ") parser.add_option( "-b", "--debug", action="store_true", default=False, dest="debug", help=u"Show debugging info") parser.add_option( "-u", "--usage", action="store_true", default=False, dest="usage", help=u"Display the six main uses for this jamu") parser.add_option( "-e", "--examples", action="store_true", default=False, dest="examples", help=u"Display examples for executing the jamu script") parser.add_option( "-v", "--version", action="store_true", default=False, dest="version", help=u"Display version and author information") parser.add_option( "-i", "--interactive", action="store_true", default=False, dest="interactive", help=u"Interactive mode allows selection of a specific Series from a series list") parser.add_option( "-f", "--flags_options", action="store_true", default=False,dest="flags_options", help=u"Display all variables and settings then exit") parser.add_option( "-l", "--language", metavar="LANGUAGE", default=u'en', dest="language", help=u"Select data that matches the specified language fall back to english if nothing found (e.g. 'es' Español, 'de' Deutsch ... etc)") parser.add_option( "-s", "--simulation", action="store_true", default=False, dest="simulation", help=u"Simulation (dry run), no downloads are performed or data bases altered") parser.add_option( "-t", "--toprated", action="store_true", default=False, dest="toprated", help=u"Only display/download the top rated TV Series graphics") parser.add_option( "-d", "--download", action="store_true", default=False, dest="download", help=u"Download and save the graphics and/or meta data") parser.add_option( "-n", "--nokeys", action="store_true", default=False, dest="nokeys", help=u"Do not add data type keys to data values when displaying data") parser.add_option( "-m", "--maximum", metavar="MAX", default=None, dest="maximum", help=u"Limit the number of graphics per type downloaded. e.g. --maximum=6") parser.add_option( "-o", "--overwrite", action="store_true", default=False, dest="overwrite", help=u"Overwrite any matching files already downloaded") parser.add_option( "-C", "--user_config", metavar="FILE", default="", dest="user_config", help=u"User specified configuration variables. e.g --user_config='~/.jamu/jamu.conf'") parser.add_option( "-F", "--filename", action="store_true", default=False, dest="ret_filename", help=u"Display a formated filename for an episode") parser.add_option( "-U", "--update", action="store_true", default=False, dest="update", help=u"Update a meta data file if local episode meta data is older than what is available on thetvdb.com") parser.add_option( "-D", "--mythtvdir", action="store_true", default=False, dest="mythtvdir", help=u"Store graphic files into the MythTV DB specified dirs") parser.add_option( "-M", "--mythtvmeta", action="store_true", default=False, dest="mythtvmeta", help=u"Add/update TV series episode or movie meta data in MythTV DB") parser.add_option( "-V", "--mythtv_verbose", action="store_true", default=False, dest="mythtv_verbose", help=u"Display verbose messages when performing MythTV metadata maintenance") parser.add_option( "-J", "--mythtvjanitor", action="store_true", default=False, dest="mythtvjanitor", help=u"Remove unused graphics (poster, fanart, banners) with the graphics janitor. Any graphics not associated with atleast one MythTV video file record is delected.") parser.add_option( "-N", "--mythtvNFS", action="store_true", default=False, dest="mythtvNFS", help=u"This option overrides Jamu's restrictions on processing NFS mounted Video and/or graphic files.") parser.add_option( "-I", "--mythtv_inetref", action="store_true", default=False, dest="mythtv_inetref", help=u"Find and interactively update any missing Interent reference numbers e.g. IMDB. This option is ONLY active if the -M option is also selected.") parser.add_option( "-W", "--mythtv_watched", action="store_true", default=False, dest="mythtv_watched", help=u"Download graphics for Scheduled and Recorded videos. This option is ONLY active if the -M option is also selected.") parser.add_option( "-G", "--mythtv_guess", action="store_true", default=False, dest="mythtv_guess", help=u"Guess at the inetref for a video. This option is ONLY active if the -M option is also selected.") parser.add_option( "-S", "--selected_data", metavar="TYPES", default=None, dest="selected_data", help=u"Select one of more data types to display or download, P-poster, B-Banner, F-Fanart, E-Episode data, I-Episode Image. e.g. --selected_data=PBFEI gets all types of data") parser.add_option( "-R", "--mythtv_ref_num", action="store_true", default=False, dest="mythtv_ref_num", help=u"Start an interactive session that ONLY adds the TVDB/TMDB reference numbers to when missing. No meta data or images will be concurrently downloaded.") opts, series_season_ep = parser.parse_args() if opts.debug: print "opts", opts print "\nargs", series_season_ep # Set the default configuration values if opts.mythtv_inetref or opts.mythtv_ref_num: opts.interactive = True configuration = Configuration(interactive = opts.interactive, debug = opts.debug) if opts.usage: # Display usage information sys.stdout.write(usage_txt+'\n') sys.exit(0) if opts.examples: # Display example information sys.stdout.write(examples_txt+'\n') sys.exit(0) if opts.version == True: # Display program information sys.stdout.write(u"\nTitle: (%s); Version: (%s); Author: (%s)\n%s\n" % ( __title__, __version__, __author__, __purpose__ )) sys.exit(0) # Verify that only one instance of the following options is running at any one time # Options (-M, -MW and -MG) options = u'' if opts.mythtvmeta: options+=u'M' else: MythLog._setlevel('none') # There cannot be any logging messages with non -M options if opts.mythtvmeta and opts.mythtv_watched: options+=u'W' if opts.mythtvmeta and opts.mythtv_guess: options+=u'G' if opts.mythtvmeta and opts.mythtvjanitor: # No instance check with the janitor option options+=u'J' if opts.mythtvmeta and opts.mythtv_inetref: # No instance check with the interactive mode option options+=u'I' if options in [u'M', u'MW', u'MG']: jamu_instance = singleinstance(u'/tmp/Jamu_%s_instance.pid' % options) # # check is another instance of Jamu is running # if jamu_instance.alreadyrunning(): print u'\n! Error: An instance of Jamu (-%s) is already running only one instance can run at a time.\nOne of the meta data sources may be off-line or very slow.\n' % options sys.exit(0) # Message the user that they are using incompatible options with the -MW option if opts.mythtvmeta and opts.mythtv_watched and (opts.mythtv_inetref or opts.interactive): print u'\n! Error: There us no Interactive mode (-I or -i) for the Jamu (-MW) option.\nPlease change your options and try again.\n' sys.exit(1) # Message the user that they are using incompatible options -R and -I or -i if opts.mythtvmeta and opts.mythtv_ref_num and opts.mythtv_inetref: print u'\n! Error: The (-R) and (-I) options are mutually exclusive.\nPlease change your options and try again.\n' sys.exit(1) # Apply any command line switches configuration.changeVariable('local_language', opts.language) configuration.changeVariable('simulation', opts.simulation) configuration.changeVariable('toprated', opts.toprated) configuration.changeVariable('download', opts.download) configuration.changeVariable('nokeys', opts.nokeys) configuration.changeVariable('maximum', opts.maximum) configuration.changeVariable('overwrite', opts.overwrite) configuration.changeVariable('ret_filename', opts.ret_filename) configuration.changeVariable('update', opts.update) configuration.changeVariable('mythtvdir', opts.mythtvdir) configuration.changeVariable('mythtvmeta', opts.mythtvmeta) configuration.changeVariable('mythtv_inetref', opts.mythtv_inetref) configuration.changeVariable('mythtv_ref_num', opts.mythtv_ref_num) configuration.changeVariable('mythtv_watched', opts.mythtv_watched) configuration.changeVariable('mythtv_guess', opts.mythtv_guess) configuration.changeVariable('mythtv_verbose', opts.mythtv_verbose) configuration.changeVariable('mythtvjanitor', opts.mythtvjanitor) configuration.changeVariable('mythtvNFS', opts.mythtvNFS) configuration.changeVariable('data_flags', opts.selected_data) # Check if the user wants to change options via a configuration file if opts.user_config != '': # Did the user want to override the default config file name/location configuration.setUseroptions(opts.user_config) else: default_config = u"%s/%s" % (os.path.expanduser(u"~"), u".mythtv/jamu.conf") if os.path.isfile(default_config): configuration.setUseroptions(default_config) else: print u"\nThere was no default Jamu configuration file found (%s)\n" % default_config if opts.flags_options: # Display option variables if len(series_season_ep): configuration.validate_setVariables(series_season_ep) else: configuration.validate_setVariables(['FAKE SERIES NAME','FAKE EPISODE NAME']) configuration.displayOptions() sys.exit(0) # Validate specific variables configuration.validate_setVariables(series_season_ep) if configuration.config['mythtvmeta']: process = MythTvMetaData(configuration.config) process.processMythTvMetaData() elif configuration.config['video_dir']: process = VideoFiles(configuration.config) results = process.processFileOrDirectory() if results != None and results != False: print process.processFileOrDirectory().encode('utf8') else: process = Tvdatabase(configuration.config) results = process.processTVdatabaseRequests() if results != None and results != False: print process.processTVdatabaseRequests().encode('utf8') return True # end main if __name__ == "__main__": main()