2 # -*- coding: UTF-8 -*-
3 # ----------------------
4 # Name: jamu.py Just.Another.Metadata.Utility
7 # Purpose: This python script is intended to perform a variety of utility functions on mythvideo
8 # metadata and the associated video files.
10 # The primary movie source for graphics and data is themoviedb.com wiki.
11 # The primary TV Series source for graphics and data is thetvdb.com wiki.
12 # Users of this script are encouraged to populate both themoviedb.com and thetvdb.com
13 # with posters, fan art and banners and meta data. The richer the source the more valuable
15 # This script uses the python module tvdb_api.py (v0.6DEV or higher) found at
16 # http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search thanks
17 # to the authors of this excellent module.
18 # The tvdb_api.py module uses the full access XML api published by thetvdb.com see:
19 # http://thetvdb.com/wiki/index.php?title=Programmers_API
20 # This python script's functionality is enhanced if you have installed "tvnamer.py" created by
21 # "dbr/Ben" who is also the author of the "tvdb_api.py" module.
22 # "tvnamer.py" is used to rename avi files with series/episode information found at
24 # Python access to the tmdb api started with a module from dbr/Ben and then enhanced for
26 # The routines to select video files was copied and modified from tvnamer.py mentioned above.
27 # The routine "_save_video_metadata_to_mythdb" has been taken and modified from
28 # "find_meta.py" author Pekka Jääskeläinen.
29 # The routine "_addCastGenre" was taken and modified from "tvdb-bulk-update.py" by
30 # author David Shilvock <davels@telus.net>.
32 # Command line examples:
33 # See help (-u and -h) options
35 # License:Creative Commons GNU GPL v2
36 # (http://creativecommons.org/licenses/GPL/2.0/)
37 #-------------------------------------
38 __title__
="JAMU - Just.Another.Metadata.Utility";
39 __author__
="R.D.Vaughan"
41 This python script is intended to perform a variety of utility functions on mythvideo metadata
42 and the associated video files.
44 The primary movie source for graphics and data is themoviedb.com wiki.
45 The primary TV Series source for graphics and data is thetvdb.com wiki.
46 Users of this script are encouraged to populate both themoviedb.com and thetvdb.com with posters,
47 fan art and banners and meta data. The richer the source the more valuable the script.
51 # 0.1.0 Initial development
52 # 0.2.0 Inital beta release
53 # 0.3.0 Add mythvideo metadata updating including movie graphics through
54 # the use of tmdb.pl when the perl script exists
55 # 0.3.1 Add mythvideo meta data add and update functionality. Intend use for
56 # maintenance cron jobs.
57 # Increase integration with mythtvideo download meta data and MythUI
58 # Added the ability to movie video files while maintaining the metadata
59 # 0.3.2 Fixed bug where some poster downloads were unnecessary
60 # Fixed bug where the mythtv database was updated for no reason
61 # Fixed bug in jamu-example.conf "min_poster_size" variable had '=' not ':'
62 # Fixed bug where a unicode URL would abort the script
63 # Using ffmpeg added setting accurate video length in minutes. A hack but
64 # lacked python method to find audio/video properties.
65 # 0.3.3 Add logic to skip any video with a inetref of '99999999'. Meta data and
66 # graphics are all manually entered and should not be altered by Jamu.
67 # Currently used for any meta data that you do not want modified by Jamu.
68 # Fixed issues with filenames containing Unicode characters.
69 # 0.3.4 Added logic to skip any secondary source meta data plot less than 10 words.
70 # Properly initialized a new record so warning messages do not display.
71 # In plot meta data replace line-feeds with a space (e.g. Space Cowboys
72 # plot contains line-feeds). Mythvideo does not expect line-feeds in a plot.
73 # Significant improvements in combining meta data between primary and
74 # secondary data sources.
75 # Remove 'tmdb.pl' calls and use the tmdb api directly.
76 # Added detection of broken symbolic links and fixed those links.
77 # Fixed inconsistencies in graphics file extentions (as received from the
78 # sources), made all extentions lowercase and changed ".jpeg" to ".jpg".
79 # 0.3.5 Fixed bug when themoviedb.com times out from an api request.
80 # A few documentation corrections.
81 # Fixed a bug with utf8 directory names.
82 # Added code to not abort script when themoviedb.com has problems. The issue
83 # is reported but the scripts continues processing.
84 # Added option "-W" to download graphics for Scheduled and Recorded videos.
85 # Change the "-J" Janitor function to avoid deleting graphics for Scheduled
86 # and Recorded videos.
87 # Fixed bug where a TMDB Poster image was not found when it was really
89 # 0.3.6 Fixed bug when searching themoviedb.com for a movie by title or
91 # Increased accuracy of non-interactive TMDB movie searching and matching.
92 # Set up for transition to TMDB's beta v2.1 api which adds language support.
93 # Corrected Watched Recording graphic file naming convention for movies.
94 # If interactive mode is selected but an exact match is found for a movie
95 # then the exact match is chosen and no interative session is initiated.
96 # Added additional messages when access to MythTV python bindings has issues.
97 # 0.3.7 Removed some redundant code.
98 # Sync up with v1.0 of tvdb_api and new way to assign tvdb api key
99 # Added an option (-MG) to allow Jamu best guessing at a video's inetref
100 # number. To guess accurately the video file name must be very close to
101 # those found on tmdb or imdb and tvdb web sites.
102 # Remove all use of the MythVideo.py "pruneMetadata" routine as it deletes
103 # records from the Mythvideo table for all video files with relative file
105 # Jamu will skip processing any videometadata which is using a Storage group.
106 # Jamu will now restrict itself to updating only videometadata records whose
107 # video files reside on the current host machine. In the case where a user
108 # has multiple backends jamu must run on each of those backends.
109 # The Janitor option (-MJ) now checks if the users has set the plugins
110 # MythGallery, MythGame and MythMusic to use the same graphics directories as
111 # MythVideo. If they share directories the Janitor option will exit
112 # without removing any graphics files. Messages indicating which directories
113 # are in conflict will be displayed.
114 # Added the detection of video or graphics on an NFS mount exiting jamu without
115 # any processing and displaying a message why this has been done. A new option
116 # for NFS (-MN) will allow a user to override this check and jamu will continue
118 # Fixed a bug when TMDB does not have a 'year' for a movie (e.g. 'Bambi')
119 # Added compatibility with or without the MythTV.py Ticket #6678
120 # Fixed a bug when ffmpeg cannot find the true length in minutes of a video
121 # Cleaned up documenation consistency with Warning and Error messages.
122 # Added to the existing TV episode video file renaming (-MF) option.
123 # Now movie video files can also be renamed to the format "title (year)"
124 # e.g. "The Duchess (2008)". If tmdb.com has no year for the movie then only
125 # the movie title will be used when renaming. Any existing metadata is
127 # 0.3.8 Made changes to sync up with MythTV trunk change set [r21138].
128 # Now handles TVDB's change from a 5 digit inetref number to 6 digits.
129 # 0.3.9 Check accessability (Read and Write) to directories and files before
130 # including them in files/directories to process.
131 # Add the ability to process Storage Groups for all Videos and graphics.
132 # Jamu now uses MythVideo.py binding's Genre and Cast routines
133 # Fixed a unicode bug with file paths.
134 # Fixed a unicode bug with some URLs containing UTF8 characters.
135 # Fixed a bug were a bad image file could avbort the script.
136 # Changed all subdirectory cover art to a copied graphic file "folder.jpg/png"
137 # to conform to the Storage Group standard. This also works for local subdirs.
138 # Fixed a bug where a TV series with out a season specific poster or
139 # banner would get repeatedly download.
140 # 0.4.0 Removed a few lines of debugging code which should never have been left in a
141 # distrubuted version.
142 # Fixed the check that confirms that all Video and graphic directories are
144 # Fixed a bug where under rare circumstances a graphic would be repeatedly
146 # Made the installation of the python IMDbPy library manditory.
147 # For all movies IMDB numbers will be used instead of converting to TMDB
148 # numbers. This is done to maintain consistency with MythVideo movie inetref
150 # 0.4.1 Fixed an obscure video file rename (-F option) error
151 # 0.4.2 Fixed a bug where bad data for either TMDB or TVDB would abort script
152 # 0.4.3 Recent changes in the MythVideo UI graphic hunts (cover art and fanart)
153 # have made Jamu's creation of "folder.xxx" graphics redundant. This
154 # feature has been turned off in Jamu. There is a new user option
155 # "folderart" that can reactivate this feature through the Jamu
156 # configuration file.
157 # 0.4.4 Changes to assist SG image hunting Jamu now adds the suffix "_coverart,
158 # fanart, _banner, _screenshot" respectively to downloaded graphics.
159 # With the use of a graphic suffix the requirement for unique graphics
160 # directories is gone. The check has been removed.
161 # 0.4.5 Fixed a bug where lowercase tv video filenames caused graphics files to
162 # also be lowercase which can cause graphics to be downloaded twice.
163 # Fixed a bug in graphics file name creation for a TV season.
164 # Added checks for compatible python library versions of xml and MySQLdb
165 # 0.4.6 Fixed a bug where a bad IMDB number in TMDB caused an abort.
166 # 0.4.7 Fixed a bug where a 'recordedprogram' record is not properly paired with a
167 # 'recorded' record. This results in no "airdate" information being available
168 # and a script abort. An airdate year of u'0000' will be assumed.
169 # Fix an abort bug when IMDB is having service problems and a list of
170 # movies cannot be retrieved.
171 # 0.4.8 Fixed a bug in a -MJ option check that removing graphics would not
172 # conflict with graphic directories for non-Mythvideo plugins.
173 # 0.4.9 Combine the video file extentions found in the "videotypes" table with those
174 # in Jamu to avoid possible issues in the (-MJ) option and to have tighter
175 # integration with MythVideo user file extention settings.
176 # 0.5.0 Fixed a bug where a filename containing invalid characters caused an abort.
177 # Such invalid filenames are now skipped with an appropriate message.
178 # Added to the -MW option the fetching of graphics from TVDB and TMDB for
179 # videos added by Miro Bridge to either Watched Recordings or MythVideo.
180 # If Miro Bridge is not being used no additional processing is performed.
181 # Two new sections ([mb_tv] and [mb_movies]) were added to the Jamu
182 # configuration file to accomodate this new functionality.
183 # The jamu configuration file now has a default name and location of
184 # "~/.mythtv/jamu.conf". This can be overridden with the command line option.
185 # This has been done so Jamu can better support Mythbuntu.
186 # Removed code that was required until ticket #6678 was committed with
188 # Filtered out checks for video run length on iso, img ... etc potentially
189 # large video files due to processing overhead especially on NFS mounts.
190 # With the -MW option skip any recordings who's recgroup is "Deleted"
191 # Fixed an abort where a TVDB TV series exists for a language but does not
192 # have a series name in other languages.
193 # 0.5.1 Fixed an abort when a user specifies secondary source input parameters
194 # that cannot be parsed from the file name. This
195 # covers secondary sources for metadata and graphics.
196 # Fixed an abort when thetvdb.com cannot be contact due to network or
198 # Added detection of erroneous graphics file downloads that are actually HTML
199 # due to source Web site issues. Jamu's (-MJ) janitor option also detects,
200 # deletes these files and repairs the MythVideo record if necessary.
201 # For the -MW option any downloaded graphics names will use the title of the
202 # recorded program instead of that found on sources like TVDB and TMDB. This
203 # resolves Watch Recordings image hunt issues when Schedule Direct uses a
204 # different program title then is on either TVDB or TMDB.
205 # Fixed an obscure bug where TVDB returns empty graphics URLs along with
206 # proper URLs. The empty URLs are now ignored.
207 # Fixed a bug when a language was specified and there were no graphics
208 # for the specified language none were returned/downloaded. This even when
209 # graphics for other languages were available. Now if there are no selected
210 # language graphics English graphics are the fall back and if there are no
211 # English graphics then any available graphics will be returned.
212 # 0.5.2 Fixed an abort when trying to add a storage group graphics without a
214 # 0.5.3 Fixed a bug where the filemarkup table is not cleaned up if Jamu renames
215 # a Miro movie trailer video file that the user wants to keep in MythVideo.
216 # Fixed a bug with Miro video file renaming of Miro Movie trailers
217 # for the same movie but which had different file extentions.
218 # 0.5.4 Conform to changeset 22104 setting of SG graphics directories to default to SG Videos if not configured.
219 # 0.5.5 Deal with TV Series and Movie titles with a "/" forward slash in their name e.g. "Face/Off"
220 # 0.5.6 Correct an issue when a user has a mixture of local and SG video records in MythVideo. Jamu was
221 # adding a hostname when the video had an absolute path. This caused issues with playback.
222 # Added more informative error messages when TMDB is returning bad xml responses.
223 # Fixed an error in the graphic file naming convention when graphics share the same download directory.
224 # 0.5.7 Remove the override of the TVDB graphics URL to the mirror site. See Kobe's comment:
225 # http://forums.thetvdb.com/viewtopic.php?f=4&t=2161#p9089
226 # 0.5.8 The issue fixed in v0.5.5 with invalid file name creation did not fully cover TV shows It does now.
227 # 0.5.9 Changed permissions checks on video directories to only require RW for the destination directories
228 # involved in the move. With this change if a user requested a file rename (-F) option and the video
229 # file does not have RW access the rename will be ignored.
230 # Uses that have their Video directories set to access and read-only can now use Jamu.
231 # Added a stdout display of the directories that Jamu will use for processing. This information may help
232 # users resolve issues. The display happens ONLY when the -V (verbose) option is used.
233 # 0.6.0 Changed The Janitor -J option to deal with graphics associated with a VIDEO_TS directory.
234 # Stopped Jamu from processing any files in a "VIDEO_TS" directory as it was leading to multiple
235 # MythVideo entires for *.VOB files. Jamu does not process multi-part videos.
236 # Added the use of PID files to prevent two instances of the same Jamu -M options running at the same
237 # time. This deals with issues when a meta data source is off line for an extended
238 # period of time and Jamu runs as a cronjob. Options effected are (-M, -MW and -MG).
239 # Change to have jamu use the TMDB Movie title as is done in MythVideo rather than the file name.
240 # Fixed a bug when TMDB genres are filtered and none remain they were still being added. This bug was
241 # spotted and correct by Mathieu Brabant (thanks).
242 # Added the ability for a user to filter additional characters from a file name this is important for
243 # users using MS-Windows file systems as a CIFS mount.
244 # 0.6.1 Added directory name parsing support for TV series title, series number and episode numbers. Patch
245 # contributed by Mitko Haralanov (thanks).
246 # 0.6.2 Added updating the 'homepage', 'releasedata' and 'hash' fields in the videometadata table
247 # is the field exists. These fields is only present in trunk.
248 # Properly initalize the homepage, hash, releasedate fields when adding a new videometadata record.
249 # 0.6.3 Convert to new python bindings and replace all direct mysql data base calls. See ticket #7264
250 # Remove the requirement for the MySQLdb python library.
251 # Removed the folder icon symlink code as it is redundant with MythVideo internal functionality.
252 # The 'folderart' option is no longer support on a jamu.conf file and will be ignored if present.
253 # Fixed a bug where a FE video directory was set but there were no FE image directories.
254 # If there were local SG images directories set they were being used. This is an invalid
255 # configuration that should have caused an error.
256 # 0.6.4 Added a new option (-R) to allow just interactively populate the video reference numbers from
257 # TVDB and TMDB without any meta data downloads. After that the user runs Jamu with the -M option
258 # and the meta data and images are downloaded.
259 # Added to the interactive interface the ability to select a reference number of '99999999'
260 # which effectively tells jamu to ignore the specific video from all further processing.
261 # Changed the return code from 1 to 0 when Jamu exits without processing if there is already an
262 # instance of Jamu running. This was causing issues for Mythbuntu when TVDB or TMDB was down for an
264 # Added a new jamu.conf section [ignore-directory] to list Video sub-directories that Jamu should
266 # Change Jamu's import of tvdb and tmdb api libraries to use the installed versions found with the
267 # MythTV python bindings.
268 # Changed Jamu to use the tmdb API v2.0 python library
269 # Jamu will always use the TMDB reference number over the IMDB number but still supports IMDB#s
270 # Jamu interactive sessions for movies now lists the TMDB#s instead of IMDB#s
271 # Jamu will convert any IMDB#s to TMDB#s when themoviedb.org includes that movie. This is in line
272 # with MythVideo changes. Graphics for the movie will also be renamed so they do not need to be
274 # Add the production countries for movies when TMDB provides that information.
275 # Adjusted the -MW option to add a " Season 1" to any downloaded image filename for TV shows.
276 # This must be done to make sure that TV shows like "24" do not clash with a movie's TMDB# like
277 # (Kill Bill Vol.1) which is "24".
278 # Added message display for exceptions where the message may enhance problem analysis.
279 # Removed logic which checked that a TV episode was using Season graphics rather than Series graphics.
280 # Unfortunately there was a chance that the a Series's graphics could clobber a movie with the same TMDB#
281 # as the series title (e.g. the movie Kill Bill Vol.1 and the TV series 24). A positive is that a number
282 # of redundant TV Series images can be removed through the jamu -MJ option.
283 # Improved the -MW options detection of TV series when the EPG data does not include a subtitle. Users
284 # can add the specific TVDB numbers to the 'series_name_override' section of the jamu.conf file.
285 # Australian users had mentioned this as an issue, previously the TV series was always being mistaken
287 # Jamu will now download the top rated TV Series season coverart and banner images. This enhancement
288 # matches MythVideo processing.
289 # 0.6.5 Small fix related to the bindings changes.
290 # 0.6.6 Fixed Exception messages
291 # Change all occurances of 'mythbeconn.host' to 'mythbeconn.hostname' to be consistent with bindings
292 # 0.6.7 Fixed the (-J) janitor option from removing the Mirobridge default images when they are not being used
293 # 0.6.8 Fixed a (-J) janitor option statistics error due to skipping Mirbridge default images
294 # 0.6.9 Fixed an abort when IMDBpy returns movie matches with incomplete data
295 # Fixed an abort where an IMDB# was being used instead of a TMDB#
296 # Fixed an abort when a storage directory name caused an UnicodeEncodeError or TypeError exception
297 # 0.7.0 Fixed an (-MW) option abort when a recorded program or upcoming program did not have a title
298 # 0.7.1 Fixed a bug where movies with punctutation ("Mr. Magoo") were not finding matches
299 # Fixed bug with interactive mode when a user enters a reference number directly rather than
300 # making a list selection
301 # These bugs were both identified by Edi Iten (thanks)
302 # 0.7.2 Fixed a bug where an inetref field was not properly initialized and caused an abort. Ticket #8243
303 # 0.7.3 Fixed a bug where a user selected TMDB# was not being used.
304 # Minor change to fuzzy matching of a file named parsed title with those from TMDB and TVDB.
305 # 0.7.4 Update for changes in Python bindings
306 # 0.7.5 Added the TMDB MovieRating as videometadata table "rating" field
307 # 0.7.6 Modifications to support MythTV python bindings changes
308 # 0.7.7 Pull hostname from python bindings instead of socket libraries
309 # Added support of unicode characters within a jamu.conf file
310 # Replace 'xml' module version check with generic Python version, to correct failure in Python 2.7
311 # 0.7.8 Fixed a bug which caused jamu to crash due to an extra unicode conversion introduced in 0.7.7.
316 JAMU - Just.Another.Metadata.Utility is a versatile utility for downloading graphics and meta data
317 for both movies and TV Series information from themoviedb.com wiki and thetvdb.com wiki. In addition
318 the MythTV data base is updated with the downloaded information.
319 Here are the main uses for this utility:
320 MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
322 1) Simple command line invocation to display or download data from thetvdb.com.
323 Data can be one or more of: Posters/Cover art, Banners, Fan art,
324 Episode Images and Episode meta data. use the command "jamu -e | less" to see
325 command line examples.
326 2) Mass downloads of data matching your video files. **
327 This typically done once to download the information for your video collection.
328 3) Automated maintenance of the information in your video collection. **
329 4) The creation of video file names which can be used to set the file name of your recorded TV shows.
330 File names can be formated to the users preference with information like series name, season number,
331 episode number and episode name. MythTV users may find this valuable as part of a user job
332 that is spawned automatically by mythbackend when recording is finished.
333 5) Jamu's modules can be imported into your own python scripts to create enhanced functionality.
334 6) With the installation of free ImageMagick's utilities (specifically 'mogrify') you can resize
335 graphics when they are downloaded.
336 7) Update the MythTV data base with links to posters, banners, fanart and episode images and optionally
337 download missing graphics if they exist. This feature can be used for mass updates and regular
343 MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
344 These examples are primarily for non-MythTV users of Jamu.
346 jamu command line examples:
347 NOTE: Included here are simple examples of jamu in action.
348 Please review jamu_README for advise on how to get the most out of jamu.
350 ( Display a TV series top rated poster fanart and banner URLs)
351 > jamu -tS PBF "Sanctuary"
352 poster:http://www.thetvdb.com/banners/posters/80159-1.jpg
353 fanart:http://www.thetvdb.com/banners/fanart/original/80159-2.jpg
354 banner:http://www.thetvdb.com/banners/graphical/80159-g2.jpg
356 ( Display the URL for a TV series episode )
357 > jamu -tS I "Fringe" 1 5
358 filename:http://www.thetvdb.com/banners/episodes/82066-391049.jpg
360 ( Display poster, fanart and banner graphics for a TV series but limited to two per type in a season )
361 > jamu -S PBF -m 2 "24" 4
362 poster:http://www.thetvdb.com/banners/seasons/76290-4-3.jpg
363 poster:http://www.thetvdb.com/banners/seasons/76290-4.jpg
364 fanart:http://www.thetvdb.com/banners/fanart/original/76290-1.jpg
365 fanart:http://www.thetvdb.com/banners/fanart/original/76290-2.jpg
366 banner:http://www.thetvdb.com/banners/seasonswide/76290-4.jpg
367 banner:http://www.thetvdb.com/banners/seasonswide/76290-4-3.jpg
369 ( Display a file name string (less file extention and directory path) for a TV episode )
371 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
373 > jamu -F "24" "Day 4: 9:00 A.M.-10:00 A.M."
374 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
376 ( Using SID number instead of series name )
378 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
380 ( Simulate a dry run for the download of a TV series top rated poster and fanart )
381 > jamu -sdtS PF "Fringe"
382 Simulation download of URL(http://www.thetvdb.com/banners/posters/82066-6.jpg) to File(~/Pictures/Poster - 82066-6.jpg)
383 Get_Poster downloading successfully processed
384 Simulation download of URL(http://www.thetvdb.com/banners/fanart/original/82066-11.jpg) to File(~/Pictures/Fanart - 82066-11.jpg)
385 Get_Fanart downloading successfully processed
387 ( Download the Episode meta data and episode image for a video file whose file name contains the series and season/episode numbers)
388 > jamu -dS EI "~/Pictures/Fringe - S01E01.mkv"
389 Episode meta data and/or images downloads successfully processed
392 60 -rw-r--r-- 1 user user 53567 2009-03-12 22:05 Fringe - S01E01 - Pilot.jpg
393 4 -rw-r--r-- 1 user user 1059 2009-03-12 22:05 Fringe - S01E01 - Pilot.meta
394 4 -rw-r--r-- 1 user user 811 2009-03-12 13:22 Fringe - S01E01.mkv
396 ( Display Episode meta data for a TV series )
401 episodename:Day 5: 9:00 A.M.-10:00 A.M.
403 overview:Jack conceals himself inside the airport hanger and surveys the Russian separatists, feeding information to Curtis and his assault team.
404 The terrorists begin executing hostages in an attempt to make Logan cave into their demands.
405 Martha discovers that all traces of her conversation with Palmer may not have been erased.
408 gueststars:John Gleeson Connolly, V.J. Foster, David Dayan Fisher, Taylor Nichols, Steve Edwards, Taras Los, Joey Munguia, Reggie Jordan, Lou Richards, Karla Zamudio
410 filename:http://www.thetvdb.com/banners/episodes/76290-306117.jpg
413 firstaired:2006-01-16
414 lastupdated:1197942225
415 productioncode:5AFF03
421 combined_episodenumber:4.0
425 dvd_episodenumber:4.0
427 ( Specify a user defined configuration file to set most of the configuration variables )
428 > jamu -C "~/.jamu/jamu.conf" -S P "Supernatural"
429 poster:http://www.thetvdb.com/banners/posters/78901-3.jpg
430 poster:http://www.thetvdb.com/banners/posters/78901-1.jpg
432 ( Display in alphabetical order the state of all configuration variables )
434 allgraphicsdir (~/Pictures)
438 debug_enabled (False)
440 ... lots of configuration variables ...
442 video_file_exts (['3gp', 'asf', 'asx', 'avi', 'mkv', 'mov', 'mp4', 'mpg', 'qt', 'rm', 'swf', 'wmv', 'm2ts', 'evo', 'ts', 'img', 'iso'])
443 with_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s)
444 without_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s)
448 import sys
, os
, re
, locale
, subprocess
, locale
, ConfigParser
, urllib
, codecs
, shutil
, datetime
, fnmatch
, string
449 from datetime
import date
450 from optparse
import OptionParser
451 from socket
import gethostbyname
452 import tempfile
, struct
455 class OutStreamEncoder(object):
456 """Wraps a stream with an encoder"""
457 def __init__(self
, outstream
, encoding
=None):
460 self
.encoding
= sys
.getfilesystemencoding()
462 self
.encoding
= encoding
464 def write(self
, obj
):
465 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
466 if isinstance(obj
, unicode):
468 self
.out
.write(obj
.encode(self
.encoding
))
477 def __getattr__(self
, attr
):
478 """Delegate everything but write to the stream"""
479 return getattr(self
.out
, attr
)
480 sys
.stdout
= OutStreamEncoder(sys
.stdout
, 'utf8')
481 sys
.stderr
= OutStreamEncoder(sys
.stderr
, 'utf8')
483 if sys
.version_info
<= (2,5):
484 print '''JAMU requires Python 2.5 or newer to run.'''
487 import xml
.etree
.cElementTree
as ElementTree
490 # Find out if the MythTV python bindings can be accessed and instances can be created
492 '''If the MythTV python interface is found, we can insert data directly to MythDB or
493 get the directories to store poster, fanart, banner and episode graphics.
495 from MythTV
import MythDB
, Video
, MythVideo
, MythBE
, MythError
, MythLog
, RecordedProgram
496 from MythTV
.database
import DBData
501 '''Create an instance of each: MythDB, MythVideo
503 MythLog
._setlevel
('none') # Some non option -M cannot have any logging on stdout
505 mythvideo
= MythVideo(mythdb
)
506 MythLog
._setlevel
('important,general')
508 print u
'\n! Warning - %s' % e
.args
[0]
509 filename
= os
.path
.expanduser("~")+'/.mythtv/config.xml'
510 if not os
.path
.isfile(filename
):
511 print u
'\n! Warning - A correctly configured (%s) file must exist\n' % filename
513 print u
'\n! Warning - Check that (%s) is correctly configured\n' % filename
515 print u
"\n! Warning - Creating an instance caused an error for one of: MythDB or MythVideo, error(%s)\n" % e
516 localhostname
= mythdb
.gethostname()
518 MythLog
._setlevel
('none') # Some non option -M cannot have any logging on stdout
519 mythbeconn
= MythBE(backend
=localhostname
, db
=mythdb
)
520 MythLog
._setlevel
('important,general')
522 print u
'\nWith any -M option Jamu must be run on a MythTV backend'
523 print u
'! Warning - %s' % e
.args
[0]
526 print u
"\n! Warning - MythTV python bindings could not be imported, error(%s)\n" % e
532 # Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available
534 # thetvdb.com specific modules
535 import MythTV
.ttvdb
.tvdb_ui
as tvdb_ui
536 # from tvdb_api import Tvdb
537 import MythTV
.ttvdb
.tvdb_api
as tvdb_api
538 from MythTV
.ttvdb
.tvdb_exceptions
import (tvdb_error
, tvdb_shownotfound
, tvdb_seasonnotfound
, tvdb_episodenotfound
, tvdb_episodenotfound
, tvdb_attributenotfound
, tvdb_userabort
)
540 # verify version of tvdbapi to make sure it is at least 1.0
541 if tvdb_api
.__version
__ < '1.0':
542 print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api
.__version
__
546 The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py.
547 They should have been installed along with the MythTV python bindings.
554 import MythTV
.tmdb
.tmdb_api
as tmdb_api
555 from MythTV
.tmdb
.tmdb_exceptions
import (TmdBaseError
, TmdHttpError
, TmdXmlError
, TmdbUiAbort
, TmdbMovieOrPersonNotFound
,)
558 The subdirectory "tmdb" containing the modules tmdb_api.py (v0.1.3 or greater), tmdb_ui.py,
559 tmdb_exceptions.py must have been installed with the MythTV python bindings.
564 if tmdb_api
.__version
__ < '0.1.3':
565 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
__)
570 try: # Check if the installation is equiped to directly search IMDB for movies
572 except ImportError, e
:
573 sys
.stderr
.write("\n! Error: To search for movies movies the IMDbPy library must be installed."\
574 "Check your installation's repository or check the following link."\
575 "from (http://imdbpy.sourceforge.net/?page=download)\nError:(%s)\n" % e
)
579 if imdb
.__version
__ < "3.8":
580 sys
.stderr
.write("\n! Error: You version the IMDbPy library (%s) is too old. You must use version 3.8 of higher." % imdb
.__version
__)
581 sys
.stderr
.write("Check your installation's repository or check the following link."\
582 "from (http://imdbpy.sourceforge.net/?page=download)\n")
585 class VideoTypes( DBData
):
586 _table
= 'videotypes'
588 _setwheredat
= 'self.intid,'
589 _logmodule
= 'Python VideoType'
591 return "<VideoTypes '%s'>" % self
.extension
593 return str(self
).encode('utf-8')
594 def __init__(self
, id=None, ext
=None, db
=None):
596 DBData
.__init
__(self
, data
=(id,), db
=db
)
597 elif ext
is not None:
598 self
.__dict
__['_where'] = 'extension=%s'
599 self
.__dict
__['_wheredat'] = 'self.extension,'
600 DBData
.__init
__(self
, data
=(ext
,), db
=db
)
602 DBData
.__init
__(self
, None, db
=db
)
605 def isValidPosixFilename(name
, NAME_MAX
=255):
606 """Checks for a valid POSIX filename
608 Filename: a name consisting of 1 to {NAME_MAX} bytes used to name a file.
609 The characters composing the name may be selected from the set of
610 all character values excluding the slash character and the null byte.
611 The filenames dot and dot-dot have special meaning.
612 A filename is sometimes referred to as a "pathname component".
614 name: (base)name of the file
615 NAME_MAX: is defined in limits.h (implementation-defined constants)
616 Maximum number of bytes in a filename
617 (not including terminating null).
618 Minimum Acceptable Value: {_POSIX_NAME_MAX}
619 _POSIX_NAME_MAX: Maximum number of bytes in a filename
620 (not including terminating null).
623 More information on http://www.opengroup.org/onlinepubs/009695399/toc.htm
625 return 1<=len(name
)<= NAME_MAX
and "/" not in name
and "\000" not in name
626 # end isValidPosixFilename()
629 # Two routines used for movie title search and matching
630 def is_punct_char(char
):
631 '''check if char is punctuation char
632 return True if char is punctuation
633 return False if char is not punctuation
635 return char
in string
.punctuation
637 def is_not_punct_char(char
):
638 '''check if char is not punctuation char
639 return True if char is not punctuation
640 return False if chaar is punctuation
642 return not is_punct_char(char
)
644 def _getExtention(URL
):
645 """Get the graphic file extension from a URL
646 return the file extention from the URL
648 (dirName
, fileName
) = os
.path
.split(URL
)
649 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
650 return fileExtension
[1:]
653 def _getFileList(dst
):
654 ''' Create an array of fully qualified file names
655 return an array of file names
661 for directory
in dst
:
663 directory
= unicode(directory
, 'utf8')
664 except (UnicodeEncodeError, TypeError):
666 for filename
in os
.listdir(directory
):
667 names
.append(os
.path
.join(directory
, filename
))
669 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
))
672 for video_file
in names
:
673 if os
.path
.isdir(video_file
):
674 new_files
= _getFileList([video_file
])
675 for new_file
in new_files
:
676 file_list
.append(new_file
)
678 file_list
.append(video_file
)
683 class singleinstance(object):
685 singleinstance - based on Windows version by Dragan Jovelic this is a Linux
686 version that accomplishes the same task: make sure that
687 only a single instance of an application is running.
690 def __init__(self
, pidPath
):
692 pidPath - full path/filename where pid for running application is to be
693 stored. Often this is ./var/<pgmname>.pid
698 # See if pidFile exists
700 if os
.path
.exists(pidPath
):
702 # Make sure it is not a "stale" pidFile
705 pid
=int(open(pidPath
, 'r').read().strip())
707 # Check list of running pids, if not running it is stale so
724 if not self
.lasterror
:
726 # Write my pid into pidFile to keep multiple copies of program from
729 fp
=open(pidPath
, 'w')
730 fp
.write(str(os
.getpid()))
733 def alreadyrunning(self
):
734 return self
.lasterror
737 if not self
.lasterror
:
739 os
.unlink(self
.pidPath
)
740 # end singleinstance()
744 graphicsDirectories
= {'banner': u'bannerdir', 'screenshot': u'episodeimagedir', 'coverfile': u'posterdir', 'fanart': u'fanartdir'}
745 dir_dict
={'posterdir': "VideoArtworkDir", 'bannerdir': 'mythvideo.bannerDir', 'fanartdir': 'mythvideo.fanartDir', 'episodeimagedir': 'mythvideo.screenshotDir', 'mythvideo': 'VideoStartupDir'}
746 storagegroupnames
= {u'Videos': u'mythvideo', u'Coverart': u'posterdir', u'Banners': u'bannerdir', u'Fanart': u'fanartdir', u'Screenshots': u'episodeimagedir'}
747 storagegroups
={u'mythvideo': [], u'posterdir': [], u'bannerdir': [], u'fanartdir': [], u'episodeimagedir': []}
# The gobal dictionary is only populated with the current hosts storage group entries
748 image_extensions
= ["png", "jpg", "bmp"]
750 def getStorageGroups():
751 '''Populate the storage group dictionary with the local host's storage groups.
754 records
= mythdb
.getStorageGroup(hostname
=localhostname
)
755 for record
in records
:
756 # Only include Video, coverfile, banner, fanart, screenshot and trailers storage groups
757 if record
.groupname
in storagegroupnames
.keys():
758 dirname
= record
.dirname
760 dirname
= unicode(record
.dirname
, 'utf8')
761 except (UnicodeDecodeError):
762 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']))
763 continue # Skip any line that has non-utf8 characters in it
764 except (UnicodeEncodeError, TypeError):
766 # Strip the trailing slash so it is consistent with all other directory paths in Jamu
767 if dirname
[-1:] == u
'/':
768 storagegroups
[storagegroupnames
[record
.groupname
]].append(dirname
[:-1])
770 storagegroups
[storagegroupnames
[record
.groupname
]].append(dirname
)
773 any_storage_group
= False
774 tmp_storagegroups
= dict(storagegroups
)
775 for key
in tmp_storagegroups
.keys():
776 if len(tmp_storagegroups
[key
]):
777 any_storage_group
= True
779 del storagegroups
[key
] # Remove empty SG directory arrays
780 if any_storage_group
:
781 # Verify that each storage group is an existing local directory
782 storagegroup_ok
= True
783 for key
in storagegroups
.keys():
784 for directory
in storagegroups
[key
]:
785 if not os
.access(directory
, os
.F_OK
):
786 sys
.stderr
.write(u
"\n! Error: The local Storage group (%s) directory (%s) does not exist\n" % (key
, directory
))
787 storagegroup_ok
= False
788 if not storagegroup_ok
:
790 # end getStorageGroups
794 """Takes a string, checks if it is numeric.
797 >>> _can_int("A test")
811 """Default non-interactive UI, which auto-selects first results
813 def __init__(self
, config
, log
):
817 def selectSeries(self
, allSeries
):
820 def selectMovieOrPerson(self
, allElements
):
821 return makeDict([allElements
[0]])
826 UI_search_language
= u
''
827 UI_selectedtitle
= u
''
828 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
829 # Hard-coded here as it is realtively static, and saves another HTTP request, as
830 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
831 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',}
833 class jamu_ConsoleUI(BaseUI
):
834 """Interactively allows the user to select a show or movie from a console based UI
837 def _displaySeries(self
, allSeries_array
):
838 """Helper function, lists series with corresponding ID
840 if video_type
== u
'IMDB':
841 URL
= u
'http://www.imdb.com/title/tt'
842 URL2
= u
'http://www.imdb.com/find?s=all&q='+urllib
.quote_plus(UI_title
.encode("utf-8"))+'&x=0&y=0'
844 elif video_type
== u
'TMDB':
845 URL
= u
'http://themoviedb.org/movie/'
846 URL2
= u
'http://themoviedb.org/'
849 URL
= u
'http://thetvdb.com/index.php?tab=series&id=%s&lid=%s'
850 URL2
= u
'http://thetvdb.com/?tab=advancedsearch'
855 for index
in range(len(allSeries_array
)):
856 allSeries
[allSeries_array
[index
]['name']] = allSeries_array
[index
]
857 tmp_names
= allSeries
.keys()
861 # Find any TV Shows or Movies who's titles start with the video's title
862 for name
in tmp_names
:
863 if filter(is_not_punct_char
, name
.lower()).startswith(filter(is_not_punct_char
, UI_title
.lower())):
864 most_likely
.append(name
)
866 # IMDB can return titles that are a movies foriegn title. The titles that do not match
867 # the requested title need to be added to the end of the most likely titles list.
868 if video_type
== u
'IMDB' and len(most_likely
):
869 for name
in tmp_names
:
871 dummy
= most_likely
.index(name
)
873 most_likely
.append(name
)
876 # Remove any name that does not start with a title like the TV Show/Movie (except for IMDB)
878 for likely
in most_likely
:
883 if not video_type
== u
'IMDB':
886 # reorder the list of series and sid's
888 for key
in names
: # list all search results
889 new_array
.append(allSeries
[key
])
891 # If there is only one to select and it is an exact match then return with no interface display
892 if len(new_array
) == 1:
893 if filter(is_not_punct_char
, allSeries_array
[0]['name'].lower()) == filter(is_not_punct_char
, UI_title
.lower()):
896 # Add the ability to select the skip inetref of '99999999'
897 new_array
.append( {'sid': '99999999', 'name': u'User choses to ignore video'}
)
898 names
.append(u
'User choses to ignore video')
901 for key
in names
: # list all search results
902 i_show
+=1 # Start at more human readable number 1 (not 0)
903 if key
== u
'User choses to ignore video':
904 print u
"% 2s -> %s # %s" % (
906 '99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
909 if video_type
!= u
'IMDB' and video_type
!= u
'TMDB':
910 tmp_URL
= URL
% (allSeries
[key
]['sid'], UI_langid_dict
[UI_search_language
])
911 print u
"% 2s -> %s # %s" % (
916 print u
"% 2s -> %s # %s%s" % (
919 allSeries
[key
]['sid']
921 print u
"Direct search of %s # %s" % (
927 def selectSeries(self
, allSeries
):
928 global UI_selectedtitle
929 UI_selectedtitle
= u
''
930 allSeries
= self
._displaySeries
(allSeries
)
932 # Check for an automatic choice
933 if len(allSeries
) <= 2:
934 for series
in allSeries
:
935 if filter(is_not_punct_char
, series
['name'].lower()) == filter(is_not_punct_char
, UI_title
.lower()):
936 UI_selectedtitle
= series
['name']
939 display_total
= len(allSeries
)
941 if video_type
== u
'IMDB':
945 elif video_type
== u
'TMDB':
950 reftype
= u
'Series id'
952 refformat
= u
"%6d" # Attempt to have the most likely TV/Movies at the top of the list
954 while True: # return breaks this loop
956 print u
'Enter choice ("Enter" key equals first selection (1)) or input the %s directly, ? for help):' % reftype
958 except KeyboardInterrupt:
959 raise tvdb_userabort(u
"User aborted (^c keyboard interupt)")
961 raise tvdb_userabort(u
"User aborted (EOF received)")
963 self
.log
.debug(u
'Got choice of: %s' % (ans
))
968 selected_id
= int(ans
) - 1 # The human entered 1 as first result, not zero
969 except ValueError: # Input was not number
971 self
.log
.debug(u
'Got quit command (q)')
972 raise tvdb_userabort(u
"User aborted ('q' quit command)")
975 print u
"# Enter the number that corresponds to the correct video."
976 print u
"# Enter the %s number for the %s." % (reftype
, video_type
)
977 print u
"# ? - this help"
980 self
.log
.debug(u
'Unknown keypress %s' % (ans
))
982 self
.log
.debug(u
'Trying to return ID: %d' % (selected_id
))
984 UI_selectedtitle
= allSeries
[selected_id
]['name']
985 return allSeries
[selected_id
]
987 if len(ans
) == refsize
and reftype
!= u
'Series id':
988 UI_selectedtitle
= u
''
989 return {'name': u'User input', 'sid': ans}
990 elif reftype
== u
'Series id':
991 if len(ans
) >= refsize
:
992 UI_selectedtitle
= u
''
993 return {'name': u'User input', 'sid': ans}
994 self
.log
.debug(u
'Invalid number entered!')
995 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)
996 UI_selectedtitle
= u
''
997 self
._displaySeries
(allSeries
)
999 #end while not valid_input
1001 def _useImageMagick(cmd
):
1002 """ Process graphics files using ImageMagick's utility 'mogrify'.
1003 >>> _useImageMagick('-resize 50% "poster.jpg"')
1007 return subprocess
.call(u
'mogrify %s > /dev/null' % cmd
, shell
=True)
1008 # end verifyImageMagick
1010 # Call a execute a command line process
1011 def callCommandLine(command
):
1012 '''Call a command line script or program. Display any errors
1013 return all stdoutput as a string
1015 p
= subprocess
.Popen(command
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
1018 data
= p
.stderr
.readline()
1020 sys
.stderr
.write(u
'%s\n' % data
)
1021 if data
== '' and p
.poll() != None:
1026 data
= p
.stdout
.readline()
1029 if data
== '' and p
.poll() != None:
1031 return returned_data
1032 # end callCommandLine
1035 # All functionality associated with configuration options
1036 class Configuration(object):
1037 """Set defaults, apply user configuration options, validate configuration settings and display the
1039 To view all available options run:
1040 >>> config = Configuration()
1041 >>> config.displayOptions()
1043 def __init__(self
, interactive
= False, debug
= False):
1044 """Initialize default configuration settings
1047 # Set all default variables
1048 self
.config
['interactive'] = interactive
1049 self
.config
['debug_enabled'] = debug
1050 self
.config
['flags_options'] = False
1051 self
.config
['local_language'] = u
'en'
1052 self
.config
['simulation'] = False
1053 self
.config
['toprated'] = False
1054 self
.config
['download'] = False
1055 self
.config
['nokeys'] = False
1056 self
.config
['maximum'] = None
1057 self
.config
['user_config'] = None
1058 self
.config
['overwrite'] = False
1059 self
.config
['update'] = False
1060 self
.config
['mythtvdir'] = False
1061 self
.config
['hd_dvd'] = ' HD - On DVD' # Used for HD DVD collection zero length video files
1062 self
.config
['dvd'] = ' - On DVD' # Used for DVD collection zero length video files
1064 self
.config
['video_dir'] = None
1065 self
.config
['recursive'] = True
1066 self
.config
['series_name'] = None
1067 self
.config
['sid'] = None
1068 self
.config
['season_num'] = None
1069 self
.config
['episode_num'] = None
1070 self
.config
['episode_name'] = None
1071 self
.config
['ret_filename'] = False
1073 # Flags for which data to perform actions on
1074 self
.config
['get_poster'] = False
1075 self
.config
['get_banner'] = False
1076 self
.config
['get_fanart'] = False
1077 self
.config
['get_ep_image'] = False
1078 self
.config
['get_ep_meta'] = False
1079 self
.config
['data_flags'] = ''
1080 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',]
1082 self
.config
['log'] = self
._initLogger
() # Setups the logger (self.log.debug() etc)
1084 # The default format of the file names (with and without episode names)
1085 self
.config
['with_ep_name'] = u
'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s'
1086 self
.config
['without_ep_name'] = u
'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s'
1087 self
.config
['ep_metadata'] = self
.config
['with_ep_name']
1089 # The default format of the graphics file names (with and without seasons and/or episode names)
1090 # The default is to use the URL's filename from thetvdb.com
1091 self
.config
['g_defaultname']=True
1092 # e.g. "Fringe - 01.jpg"
1093 self
.config
['g_series'] = u
'%(series)s - %(seq)s.%(ext)s'
1094 # e.g. "SG-1 - 07-02.jpg"
1095 self
.config
['g_season'] = u
'%(series)s - %(seasonnumber)02d-%(seq)s.%(ext)s'
1097 # Set default configuration variables
1098 # Start - Variables the user can override through option "-u" with their own file of variables
1099 self
.config
['allgraphicsdir'] = os
.getcwd()
1100 self
.config
['posterdir'] = None
1101 self
.config
['bannerdir'] = None
1102 self
.config
['fanartdir'] = None
1103 self
.config
['episodeimagedir'] = None
1104 self
.config
['metadatadir'] = None
1105 self
.config
['mythtvmeta'] = False
1106 self
.config
['myth_secondary_sources'] = {}
1107 self
.config
['posterresize'] = False
1108 self
.config
['fanartresize'] = False
1109 self
.config
['min_poster_size'] = 400
1110 self
.config
['image_library'] = False
1111 self
.config
['ffmpeg'] = True
1112 self
.config
['folderart'] = False
1113 self
.config
['metadata_exclude_as_update_trigger'] = ['intid', 'season', 'episode', 'showlevel', 'filename', 'coverfile', 'childid', 'browse', 'playcommand', 'trailer', 'host', 'screenshot', 'banner', 'fanart']
1114 self
.config
['filename_char_filter'] = u
"/%\000"
1115 self
.config
['ignore-directory'] = []
1118 # Dictionaries for Miro Bridge metadata downlods
1119 self
.config
['mb_tv_channels'] = {}
1120 self
.config
['mb_movies'] = {}
1122 # Episode data keys that you want to display or download.
1123 # This includes the order that you want them display or in the downloaded file.
1124 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']
1126 self
.config
['config_file'] = False
1127 self
.config
['series_name_override'] = False
1128 self
.config
['ep_name_massage'] = False
1129 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']
1132 # Regex pattern strings used to check for season number from directory names
1133 self
.config
['season_dir_pattern'] = [
1135 re
.compile(u
'''^.+?[ \._\-]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1137 re
.compile(u
'''^.+?[ \._\-][Ss]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1139 re
.compile(u
'''([0-9]+)[^\\/]*$''', re
.UNICODE
),
1141 re
.compile(u
'''[Ss]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1145 # Set default regex pattern strings used to extract series name , season and episode numbers for file name
1146 self
.config
['name_parse'] = [
1148 re
.compile(u
'''^(.+?)[ \._\-]\[[Ss]([0-9]+?)\]_\[[Ee]([0-9]+?)\]?[^\\/]*$''', re
.UNICODE
),
1150 re
.compile(u
'''^(.+?)[ \._\-]\[?([0-9]+)x([0-9]+)[^\\/]*$''', re
.UNICODE
),
1151 # foo.s01.e01, foo.s01_e01
1152 re
.compile(u
'''^(.+?)[ \._\-][Ss]([0-9]+)[\.\- ]?[Ee]([0-9]+)[^\\/]*$''' , re
.UNICODE
),
1154 re
.compile(u
'''^(.+)[ \._\-]([0-9]{1})([0-9]{2})[\._ -][^\\/]*$''' , re
.UNICODE
),
1156 re
.compile(u
'''^(.+)[ \._\-]([0-9]{2})([0-9]{2,3})[\._ -][^\\/]*$''' , re
.UNICODE
),
1159 # regex strings to parse folder names for TV series title, season and episode numbers
1160 self
.config
['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': u'Season', u'episode': u'Episode'}
1161 self
.config
['fullname_parse_regex'] = [
1162 # Title/Season 1/s01e01 Subtitle
1163 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1164 u
'''(?P<seasno>[0-9]+)/[Ss][0-9]+[Ee](?P<epno>[0-9]+).+$''',
1166 # Title/Season 1/1x01 Subtitle
1167 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1168 u
'''[0-9]+/(?P<seasno>[0-9]+)[Xx](?P<epno>[0-9]+).+$''',
1169 # Title/Season 1/01 Subtitle
1170 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1171 u
'''(?P<seasno>[0-9]+)/(?P<epno>[0-9]+).+$''',
1172 # Title/Season 1/Title s01e01 Subtitle
1173 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1174 u
'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s [Ss][0-9]+'''+
1175 u
'''[Ee](?P<epno>[0-9]+).+$''',
1176 # Title/Season 1/Title 1x01 Subtitle
1177 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1178 u
'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))'''+
1179 u
'''[Xx](?P<epno>[0-9]+).+$''',
1180 # Title/Season 1/Episode 1 Subtitle
1181 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1182 u
'''(?P<seasno>[0-9]+)/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1183 # Title/Season 1/Season 1 Episode 1 Subtitle
1184 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1185 u
'''(?P<seasno>[0-9]+)/%(season)s%(slash)s (?:(?P=seasno))%(slash)s '''+
1186 u
'''%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1187 # Title Season 1/01 Subtitle
1188 u
'''^.+?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1189 u
'''/(?P<epno>[0-9]+).+$''',
1190 # Title Season 1/s01e01 Subtitle
1191 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1192 u
'''/[Ss][0-9]+[Ee](?P<epno>[0-9]+).+''',
1193 # Title Season 1/1x01 Subtitle
1194 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1195 u
'''/(?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1196 # Title Season 1/Title s01e01 Subtitle
1197 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1198 u
'''/(?:(?P=seriesname))%(slash)s [Ss][0-9]+[Ee](?P<epno>[0-9]+).+$''',
1199 # Title Season 1/Title 1x01 Subtitle
1200 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1201 u
'''/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1202 # Title Season 1/Episode 1 Subtitle
1203 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1204 u
'''/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1205 # Title Season 1/Season 1 Episode 1 Subtitle
1206 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1207 u
'''/%(season)s%(slash)s (?:(?P=seasno))%(slash)s %(episode)s%(slash)s (?P<epno>[0-9]+).+$'''
1210 # Initalize a valriable used by the -MW option
1211 self
.program_seriesid
= None
1212 self
.config
[u
'file_move_flag'] = False
1217 data_flags_table
={ 'P': 'get_poster', 'B': 'get_banner', 'F': 'get_fanart', 'I': 'get_ep_image', 'E': 'get_ep_meta'}
1220 def _initLogger(self
):
1221 """Sets up a logger using the logging module, returns a log object
1223 logger
= logging
.getLogger(u
"jamu")
1224 formatter
= logging
.Formatter(u
'%(asctime)s) %(levelname)s %(message)s')
1226 hdlr
= logging
.StreamHandler(sys
.stdout
)
1228 hdlr
.setFormatter(formatter
)
1229 logger
.addHandler(hdlr
)
1231 if self
.config
['debug_enabled']:
1232 logger
.setLevel(logging
.DEBUG
)
1234 logger
.setLevel(logging
.WARNING
)
1238 def setUseroptions(self
, useroptions
):
1239 """ Change variables through a user supplied configuration file
1240 return False and exit the script if there are issues with the configuration file values
1242 if useroptions
[0]=='~':
1243 useroptions
=os
.path
.expanduser("~")+useroptions
[1:]
1244 if os
.path
.isfile(useroptions
) == False:
1246 "\n! Error: The specified user configuration file (%s) is not a file\n" % useroptions
1249 cfg
= ConfigParser
.SafeConfigParser()
1250 cfg
.readfp(codecs
.open(useroptions
, "r", "utf8"))
1251 for section
in cfg
.sections():
1252 if section
[:5] == 'File ':
1253 self
.config
['config_file'] = section
[5:]
1255 if section
== 'variables':
1256 # Change variables per user config file
1257 for option
in cfg
.options(section
):
1258 if option
== 'video_file_exts' or option
== 'tmdb_genre_filter' or option
== 'metadata_exclude_as_update_trigger':
1259 tmp_list
= (cfg
.get(section
, option
).rstrip()).split(',')
1260 for i
in range(len(tmp_list
)): tmp_list
[i
] = (tmp_list
[i
].strip()).lower()
1261 self
.config
[option
] = tmp_list
1263 if option
== 'filename_char_filter':
1264 for char
in cfg
.get(section
, option
):
1265 self
.config
['filename_char_filter']+=char
1267 if option
== 'translate':
1268 s_e
= (cfg
.get(section
, option
).rstrip()).split(',')
1269 if not len(s_e
) == 2:
1271 for index
in range(len(s_e
)):
1272 s_e
[index
] = s_e
[index
].strip()
1273 self
.config
['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': s_e[0], u'episode': s_e[1]}
1276 # Ignore user settings for Myth Video and graphics file directories
1277 # when the MythTV metadata option (-M) is selected
1278 if self
.config
['mythtvmeta'] and option
in ['posterdir', 'bannerdir', 'fanartdir', 'episodeimagedir', 'mythvideo']:
1280 self
.config
[option
] = cfg
.get(section
, option
)
1282 if section
== 'regex':
1283 # Change variables per user config file
1284 for option
in cfg
.options(section
):
1285 self
.config
['name_parse'].append(re
.compile(cfg
.get(section
, option
), re
.UNICODE
))
1287 if section
== 'ignore-directory':
1288 # Video directories to be excluded from Jamu processing
1289 for option
in cfg
.options(section
):
1290 self
.config
['ignore-directory'].append(cfg
.get(section
, option
))
1292 if section
=='series_name_override':
1294 for option
in cfg
.options(section
):
1295 overrides
[option
] = cfg
.get(section
, option
)
1296 if len(overrides
) > 0:
1297 self
.config
['series_name_override'] = overrides
1299 if section
=='ep_name_massage':
1301 for option
in cfg
.options(section
):
1302 tmp
=cfg
.get(section
, option
).split(',')
1303 if len(tmp
)%2 and len(cfg
.get(section
, option
)) != 0:
1304 sys
.stderr
.write(u
"\n! Error: For (%s) 'ep_name_massage' values must be in pairs\n" % option
)
1308 while i
!= len(tmp
):
1309 tmp
[i
] = tmp
[i
].strip()
1310 tmp
[i
+1] = tmp
[i
+1].strip()
1311 tmp_array
.append([tmp
[i
].replace('"',''), tmp
[i
+1].replace('"','')])
1313 massage
[option
]=tmp_array
1314 if len(massage
) > 0:
1315 self
.config
['ep_name_massage'] = massage
1317 if section
== 'ep_metadata_to_download':
1318 if len(cfg
.options(section
)):
1319 if cfg
.options(section
)[0] == 'ep_include_data':
1320 tmp
=cfg
.get(section
, cfg
.options(section
)[0])
1321 overrides
=tmp
.split(',')
1322 for index
in range(len(overrides
)):
1323 x
= overrides
[index
].replace(' ','')
1327 del overrides
[index
]
1328 self
.config
['ep_include_data']=overrides
1330 if section
== 'data_flags':
1331 if len(cfg
.options(section
)):
1332 for option
in cfg
.options(section
):
1333 if cfg
.get(section
, option
).lower() != 'False'.lower():
1334 for key
in self
.data_flags_table
.keys():
1335 if option
== self
.data_flags_table
[key
]:
1336 self
.config
[option
] = True
1338 for sec
in ['movies-secondary-sources', 'tv-secondary-sources']:
1341 for option
in cfg
.options(section
):
1342 secondary
[option
] = cfg
.get(section
, option
)
1343 if len(secondary
) > 0:
1344 self
.config
['myth_secondary_sources'][sec
[:sec
.index('-')]] = secondary
1346 if section
== u
'mb_tv':
1347 # Add the channel names and their corresponding thetvdb.com id numbers
1348 for option
in cfg
.options(section
):
1349 self
.config
['mb_tv_channels'][filter(is_not_punct_char
, option
.lower())] = [cfg
.get(section
, option
), u
'']
1351 if section
== u
'mb_movies':
1352 # Add the channel names for movie trailer Channels
1353 for option
in cfg
.options(section
):
1354 self
.config
['mb_movies'][filter(is_not_punct_char
, option
.lower())] = cfg
.get(section
, option
)
1357 # Expand any home directories that are not fully qualified
1358 dirs_to_check
= [u
'bannerdir', u
'episodeimagedir', u
'metadatadir', u
'posterdir', u
'video_dir', u
'fanartdir']
1359 for item
in dirs_to_check
:
1360 if self
.config
[item
]:
1361 if item
== u
'metadatadir' and not self
.config
[item
]:
1363 if self
.config
[item
][0]=='~':
1364 self
.config
[item
]=os
.path
.expanduser("~")+self
.config
[item
][1:]
1367 def displayOptions(self
):
1368 """ Display all of the configuration values. This is used to verify that the user has the
1369 variables set as they want before running jamu live.
1371 keys
=self
.config
.keys()
1374 ################### Used to create the example configuration file "jamu-example-conf"
1375 # for key in keys: # Used to create the example configuration file "jamu-example-conf"
1376 # print "#%s: %s" % (key, self.config[key])
1381 if key
== 'log': # Do not display the logger instance it is irrelevant for display
1384 if key
== 'name_parse':
1385 print u
"%s (%d items)" % (key
, len(self
.config
[key
]))
1387 print u
"%s (%s)" % (key
, str(self
.config
[key
]))
1390 print u
"%s (%d items)" % (key
, len(self
.config
[key
]))
1392 print u
"%s:" % key
, self
.config
[key
]
1393 # end set_Userconfig
1395 def changeVariable(self
, key
, value
):
1396 """Change any configuration variable - caution no validation is preformed
1398 self
.config
[key
]=value
1399 # end changeVariable
1402 def _checkNFS(self
, dirs
, ext_filter
):
1403 '''Check if any of the files are on NFS shares. If they are then the user must be warned.
1404 return True if there are at least one file is on a NFS share.
1405 return False if no graphic files are on an NFS share.
1408 for d
in dirs
: # Get rid of Null directories
1413 global localhostname
, graphicsDirectories
1415 localip
= gethostbyname(localhostname
) # Get the local hosts IP address
1416 except Exception, e
:
1417 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
))
1420 # Get all curently mounted NFS shares
1421 tmp_mounts
= callCommandLine("mount -l | grep '//'").split('\n')
1423 for mount
in tmp_mounts
:
1425 parts
= mount
.split(' ')
1426 tmparray
=[P
for P
in parts
]
1427 if tmparray
[0].startswith('//'): # Is this a NFS share definition
1428 if not tmparray
[0].startswith(u
'//%s' % localip
) and not tmparray
[0].startswith(u
'//%s' % localhostname
):
1429 nfs
.append(tmparray
[2]) # Add an NFS mount name
1431 if not len(nfs
): # Check if there are any NFS mounts
1434 # Check if any of the directories have files on an NFS share
1435 for directory
in dirs
: # Check the base directories first
1437 if os
.path
.realpath(directory
).startswith(mount
):
1439 for directory
in dirs
: # Check the actual files
1440 file_list
= _getFileList([directory
])
1441 if not len(file_list
):
1444 for fle
in file_list
: # Make a copy of file_list
1445 tmp_list
.append(fle
)
1446 for g_file
in tmp_list
: # Cull the list removing dirs and non-extention files
1447 if os
.path
.isdir(g_file
):
1448 file_list
.remove(g_file
)
1450 g_ext
= _getExtention(g_file
)
1451 if not g_ext
.lower() in ext_filter
:
1452 file_list
.remove(g_file
)
1454 for filename
in file_list
: # Actually check each file against the NFS mounts
1456 if os
.path
.realpath(filename
).startswith(mount
):
1462 def _getMythtvDirectories(self
):
1463 """Get all graphics directories found in the MythTV DB and change their corresponding
1464 configuration values. /media/video:/media/virtual/VB_Share/Review
1466 # Stop processing if this local host has any storage groups
1467 global localhostname
, storagegroups
1468 # Make sure Jamu is being run on a MythTV backend
1470 sys
.stderr
.write(u
"\n! Error: Jamu must be run on a MythTV backend. Local host (%s) is not a MythTV backend.\n" % localhostname
)
1474 for key
in dir_dict
.keys():
1475 graphics_dir
= mythdb
.settings
[localhostname
][dir_dict
[key
]]
1476 # Only use path from MythTV if one was found
1477 self
.config
[key
] = []
1478 if key
== 'mythvideo' and graphics_dir
:
1479 tmp_directories
= graphics_dir
.split(':')
1480 if len(tmp_directories
):
1481 for i
in range(len(tmp_directories
)):
1482 tmp_directories
[i
] = tmp_directories
[i
].strip()
1483 if tmp_directories
[i
] != '':
1484 if os
.access(tmp_directories
[i
], os
.F_OK
):
1485 self
.config
[key
].append(tmp_directories
[i
])
1488 sys
.stderr
.write(u
"\n! Warning: MythTV video directory (%s) does not exist.\n" % (tmp_directories
[i
]))
1491 if key
!= 'mythvideo' and graphics_dir
:
1492 if os
.path
.os
.access(graphics_dir
, os
.F_OK
):
1493 self
.config
[key
] = [graphics_dir
]
1495 sys
.stderr
.write(u
"\n! Warning: MythTV (%s) directory (%s) does not exist.\n" % (key
, graphics_dir
))
1497 # Save the FE path settings local to this backend
1498 self
.config
['localpaths'] = {}
1499 for key
in dir_dict
.keys():
1500 self
.config
['localpaths'][key
] = []
1502 if len(self
.config
[key
]):
1503 self
.config
['localpaths'][key
] = list(self
.config
[key
])
1505 # If there is a Videos SG then there is always a Graphics SG using Videos as a fallback
1507 for key
in dir_dict
.keys():
1508 if key
== 'episodeimagedir' or key
== 'mythvideo':
1510 if storagegroups
.has_key(u
'mythvideo') and not storagegroups
.has_key(key
):
1511 storagegroups
[key
] = list(storagegroups
[u
'mythvideo']) # Set fall back
1513 # Use Storage Groups as the priority but append any FE directory settings that
1514 # are local to this BE but are not already used as a storage group
1515 if storagegroups
.has_key(u
'mythvideo'):
1516 for key
in storagegroups
.keys():
1517 self
.config
[key
] = list(storagegroups
[key
])
1518 for k
in self
.config
['localpaths'][key
]:
1519 if not k
in self
.config
[key
]:
1520 self
.config
[key
].append(k
) # Add any FE settings local directories not already included
1522 if key
== 'mythvideo':
1523 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
))
1525 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
))
1528 # Make sure there is a directory set for Videos and other graphics directories on this host
1530 for key
in dir_dict
.keys():
1531 if key
== 'episodeimagedir': # Jamu does nothing with Screenshots
1533 # The fall back graphics SG is the Videos SG directory as of changeset 22104
1534 if storagegroups
.has_key(u
'mythvideo') and not len(self
.config
[key
]):
1535 self
.config
[key
] = storagegroups
[u
'mythvideo']
1536 if not len(self
.config
[key
]):
1537 sys
.stderr
.write(u
"\n! Error: There must be a directory for Videos and each graphic type. The (%s) directory is missing.\n" % (key
))
1542 # Make sure that the directory set for Videos and other graphics directories have the proper permissions
1544 for key
in dir_dict
.keys():
1545 for directory
in self
.config
[key
]:
1546 if key
== 'episodeimagedir': # Jamu does nothing with Screenshots
1548 if key
== 'mythvideo':
1549 if not os
.access(directory
, os
.F_OK | os
.R_OK
):
1550 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
, ))
1553 if not os
.access(directory
, os
.F_OK | os
.R_OK | os
.W_OK
):
1554 sys
.stderr
.write(u
"\n! Error: The (%s) directory (%s) must be read/writable for Jamu to function.\n" % (key
, directory
, ))
1559 # Print out the video and image directories that will be used for processing
1560 if self
.config
['mythtv_verbose']:
1561 dir_types
={'posterdir': "Cover art ", 'bannerdir': 'Banners ', 'fanartdir': 'Fan art ', 'episodeimagedir': 'Screenshots', 'mythvideo': 'Video '}
1562 sys
.stdout
.write(u
"\n==========================================================================================\n")
1563 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
)
1564 sys
.stdout
.write(u
"Note: All directories are from settings in the MythDB specific to hostname (%s).\n" % localhostname
)
1565 sys
.stdout
.write(u
"Note: Screenshot directories are not listed as Jamu does not process Screenshots.\n")
1566 sys
.stdout
.write(u
"------------------------------------------------------------------------------------------\n")
1567 for key
in dir_dict
.keys():
1568 if key
== 'episodeimagedir':
1570 for directory
in self
.config
[key
]:
1572 if storagegroups
.has_key(key
):
1573 if directory
in storagegroups
[key
]:
1575 sys
.stdout
.write(u
"Type: %s - SG-%s - Directory: (%s)\n" % (dir_types
[key
], sg_flag
, directory
))
1576 sys
.stdout
.write(u
"------------------------------------------------------------------------------------------\n")
1577 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")
1578 sys
.stdout
.write(u
"==========================================================================================\n\n")
1580 if self
.config
[u
'file_move_flag']: # verify the destination directory in a move is read/writable
1583 for arg
in self
.args
:
1587 if not os
.access(arg
, os
.F_OK
):
1588 for dirct
in self
.config
['mythvideo']:
1589 if arg
.startswith(dirct
):
1590 if not os
.access(dirct
, os
.F_OK | os
.R_OK | os
.W_OK
):
1591 sys
.stderr
.write(u
"! Error: Your move destination root MythVideo directory (%s) must be read/writable for Jamu to function.\n\n" % (dirct
, ))
1595 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
, ))
1597 elif not os
.access(arg
, os
.F_OK | os
.R_OK | os
.W_OK
):
1598 sys
.stderr
.write(u
"! Error: Your move destination directory (%s) must be read/writable for Jamu to function.\n\n" % (arg
, ))
1604 # Check if any Video files are on a NFS shares
1605 if not self
.config
['mythtvNFS']: # Maybe the NFS check is to be skipped
1606 if self
._checkNFS
(self
.config
['mythvideo'], self
.config
['video_file_exts']):
1607 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")
1609 # end _getMythtvDirectories
1612 def _JanitorConflicts(self
):
1613 '''Verify that there are no conflict between the graphics directories of MythVideo and
1614 other MythTV plugins. Write an warning message if a conflict is found.
1615 return True when there is a conflict
1616 return False when there is no conflict
1618 # Except for the plugins below no other plugins have non-theme graphics
1620 # Table 'settings' fields 'GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation'
1622 # Table 'settings' fields 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir'
1624 # Table 'settings' fields 'MusicLocation'
1625 global graphicsDirectories
, localhostname
1626 tablefields
= ['GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation', 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir', 'MusicLocation', 'ScreenShotPath']
1627 returnvalue
= False # Initalize as no conflicts
1628 for field
in tablefields
:
1629 tmp_setting
= mythdb
.settings
[localhostname
][field
]
1632 settings
= tmp_setting
.split(':') # Account for multiple dirs per setting
1633 if not len(settings
):
1635 for setting
in settings
:
1636 for directory
in graphicsDirectories
.keys():
1637 if not self
.config
[graphicsDirectories
[directory
]]:
1639 # As the Janitor processes subdirectories matching must be a starts with check
1640 for direc
in self
.config
[graphicsDirectories
[directory
]]:
1641 if os
.path
.realpath(setting
).startswith(os
.path
.realpath(direc
)):
1642 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
]]) )
1645 # end _JanitorConflicts
1648 def _addMythtvUserFileTypes(self
):
1649 """Add video file types to the jamu list from the "videotypes" table
1651 # Get videotypes table field names:
1653 records
= VideoTypes
.getAllEntries(mythdb
)
1654 except MythError
, e
:
1655 sys
.stderr
.write(u
"\n! Error: Reading videotypes MythTV table: %s\n" % e
.args
[0])
1658 for record
in records
:
1659 # Remove any extentions that are in Jamu's list but the user wants ignore
1661 if record
.extension
in self
.config
['video_file_exts']:
1662 self
.config
['video_file_exts'].remove(record
.extension
)
1663 if record
.extension
.lower() in self
.config
['video_file_exts']:
1664 self
.config
['video_file_exts'].remove(record
.extension
.lower())
1665 else: # Add extentions that are not in the Jamu list
1666 if not record
.extension
in self
.config
['video_file_exts']:
1667 self
.config
['video_file_exts'].append(record
.extension
)
1668 # Make sure that all video file extensions are lower case
1669 for index
in range(len(self
.config
['video_file_exts'])):
1670 self
.config
['video_file_exts'][index
] = self
.config
['video_file_exts'][index
].lower()
1671 # end _addMythtvUserFileTypes()
1674 def validate_setVariables(self
, args
):
1675 """Validate the contents of specific configuration variables
1676 return False and exit the script if an invalid configuation value is found
1678 # Fix all variables which were changed by a users configuration files
1679 # to 'None', 'False' and 'True' literals back to their intended values
1680 keys
=self
.config
.keys()
1681 types
={'None': None, 'False': False, 'True': True}
1683 for literal
in types
.keys():
1684 if self
.config
[key
] == literal
:
1685 self
.config
[key
] = types
[literal
]
1687 # Compile regex strings to parse folder names for TV series title, season and episode numbers
1688 self
.config
['fullname_parse'] = []
1689 for index
in range(len(self
.config
['fullname_parse_regex'])):
1690 self
.config
['fullname_parse'].append(re
.compile(self
.config
['fullname_parse_regex'][index
] % self
.config
['fullname_parse_season_episode_translation'], re
.UNICODE
))
1692 if self
.config
['mythtvmeta']:
1693 if mythdb
== None or mythvideo
== None:
1694 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")
1698 self
.config
['image_library'] = Image
1699 except Exception, e
:
1700 sys
.stderr
.write(u
"""\n! Error: Python Imaging Library is required for figuring out the sizes of
1701 the fetched poster images.
1703 In Debian/Ubuntu it is packaged as 'python-imaging'.
1704 http://www.pythonware.com/products/pil/\nError:(%s)\n""" % e
)
1707 if not _can_int(self
.config
['min_poster_size']):
1708 sys
.stderr
.write(u
"\n! Error: The poster minimum value must be an integer (%s)\n" % self
.config
['min_poster_size'])
1711 self
.config
['min_poster_size'] = int(self
.config
['min_poster_size'])
1713 if self
.config
['maximum'] != None:
1714 if _can_int(self
.config
['maximum']) == False:
1715 sys
.stderr
.write(u
"\n! Error: Maximum option is not an integer (%s)\n" % self
.config
['maximum'])
1718 # Detect if this is a move request
1719 self
.config
[u
'file_move_flag'] = False
1721 if os
.path
.isfile(args
[0]) or os
.path
.isdir(args
[0]) or args
[0][-1:] == '*':
1722 self
.config
[u
'file_move_flag'] = True
1723 self
.args
= list(args
)
1725 if self
.config
['mythtvdir']:
1726 if mythdb
== None or mythvideo
== None:
1727 sys
.stderr
.write(u
"\n! Error: MythTV python interface is not available\n")
1729 if self
.config
['mythtvdir'] or self
.config
['mythtvmeta']:
1730 self
._addMythtvUserFileTypes
() # add user filetypes from the "videotypes" table
1731 self
._getMythtvDirectories
()
1732 if self
.config
['mythtvjanitor']: # Check for graphic directory conflicts with other plugins
1733 if self
._JanitorConflicts
():
1735 if not self
.config
['mythtvNFS']:
1736 global graphicsDirectories
, image_extensions
1738 for key
in graphicsDirectories
:
1739 if key
!= u
'screenshot':
1740 for directory
in self
.config
[graphicsDirectories
[key
]]:
1741 dirs
.append(directory
)
1742 # Check if any Graphics files are on NFS shares
1743 if self
._checkNFS
(dirs
, image_extensions
):
1744 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")
1747 if self
.config
['posterresize'] != False or self
.config
['fanartresize'] != False:
1748 if _useImageMagick("-version"):
1749 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'])))
1752 if self
.config
['mythtvmeta'] and len(args
) == 0:
1756 sys
.stderr
.write(u
"\n! Error: At least a video directory, SID or season name must be supplied\n")
1759 if os
.path
.isfile(args
[0]) or os
.path
.isdir(args
[0]) or args
[0][-1:] == '*':
1760 self
.config
['video_dir'] = []
1762 self
.config
['video_dir'].append(unicode(arg
,'utf8'))
1763 elif not self
.config
['mythtvmeta']:
1764 if _can_int(args
[0]) and len(args
[0]) >= 5:
1765 self
.config
['sid'] = unicode(args
[0], 'utf8') # There is still a chance that this is a series name "90210"
1767 if self
.config
['series_name_override']:
1768 if self
.config
['series_name_override'].has_key(args
[0].lower()):
1769 self
.config
['sid'] = unicode((self
.config
['series_name_override'][args
[0].lower()]).strip(), 'utf8')
1771 self
.config
['series_name'] = unicode(args
[0].strip(), 'utf8')
1773 self
.config
['series_name'] = unicode(args
[0].strip(), 'utf8')
1776 sys
.stderr
.write("\n! Error: Too many arguments (%d), maximum is three.\n" % len(args
))
1777 print "! args:", args
1779 if len(args
) == 3 and _can_int(args
[1]) and _can_int(args
[2]):
1780 self
.config
['season_num'] = args
[1]
1781 self
.config
['episode_num'] = args
[2]
1782 elif len(args
) == 3:
1783 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]))
1785 elif len(args
) == 2 and _can_int(args
[1]):
1786 self
.config
['season_num'] = args
[1]
1788 if self
.config
['ep_name_massage']:
1789 if self
.config
['ep_name_massage'].has_key(self
.config
['series_name']):
1790 tmp_ep_name
=args
[1].strip()
1791 tmp_array
=self
.config
['ep_name_massage'][self
.config
['series_name']]
1792 for pair
in tmp_array
:
1793 tmp_ep_name
= tmp_ep_name
.replace(pair
[0],pair
[1])
1794 self
.config
['episode_name'] = unicode(tmp_ep_name
, 'utf8')
1796 self
.config
['episode_name'] = unicode(args
[1].strip(), 'utf8')
1798 self
.config
['episode_name'] = unicode(args
[1].strip(), 'utf8')
1800 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
1801 # Hard-coded here as it is realtively static, and saves another HTTP request, as
1802 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
1803 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"]
1805 # Validate language as specified by user
1806 if self
.config
['local_language']:
1807 if not self
.config
['local_language'] in valid_languages
:
1809 for lang
in valid_languages
: valid_langs
+= lang
+', '
1810 valid_langs
=valid_langs
[:-2]
1811 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
))
1813 global UI_search_language
1814 UI_search_language
= self
.config
['local_language']
1816 if self
.config
['data_flags']:
1817 for data_type
in self
.config
['data_flags']:
1818 if self
.data_flags_table
.has_key(data_type
):
1819 self
.config
[self
.data_flags_table
[data_type
]]=True
1820 # end validate_setVariables
1823 """Return a copy of the configuration variables
1827 # end class Configuration
1830 class Tvdatabase(object):
1831 """Process direct thetvdb.com requests
1833 def __init__(self
, configuration
):
1834 """Retrieve all configuration options and get an instance of tvdb_api which is used to
1835 access thetvdb.com wiki.
1837 self
.config
= configuration
1838 cache_dir
=u
"/tmp/tvdb_api_%s/" % os
.geteuid()
1839 if self
.config
['interactive']:
1840 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)
1842 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)
1845 # High level dictionay keys for select graphics URL(s)
1846 fanart_key
=u
'fanart'
1847 banner_key
=u
'series'
1848 poster_key
=u
'poster'
1849 season_key
=u
'season'
1850 # Lower level dictionay keys for select graphics URL(s)
1851 poster_series_key
=u
'680x1000'
1852 poster_season_key
=u
'season'
1853 fanart_hires_key
=u
'1920x1080'
1854 fanart_lowres_key
=u
'1280x720'
1855 banner_series_key
=u
'graphical'
1856 banner_season_key
=u
'seasonwide'
1857 # Type of graphics being requested
1858 poster_type
=u
'poster'
1859 fanart_type
=u
'fanart'
1860 banner_type
=u
'banner'
1861 ep_image_type
=u
'filename'
1863 def sanitiseFileName(self
, name
):
1864 '''Take a file name and change it so that invalid or problematic characters are substituted with a "_"
1865 return a sanitised valid file name
1867 if name
== None or name
== u
'':
1869 for char
in self
.config
['filename_char_filter']:
1870 name
= name
.replace(char
, u
'_')
1872 name
= u
'_'+name
[1:]
1874 # end sanitiseFileName()
1877 def _getSeriesBySid(self
, sid
):
1878 """Lookup a series via it's sid
1879 return tvdb_api Show instance
1881 seriesid
= u
'sid:' + sid
1882 if not self
.corrections
.has_key(seriesid
):
1883 self
._getShowData
(sid
)
1884 self
.corrections
[seriesid
] = sid
1885 return self
.shows
[sid
]
1886 tvdb_api
.Tvdb
.series_by_sid
= _getSeriesBySid
1887 # end _getSeriesBySid
1889 def _searchforSeries(self
, sid_or_name
):
1890 """Get TV series data by sid or series name
1891 return None if the TV show was not found
1892 return an tvdb_api instance of the TV show data if it was found
1894 if self
.config
['sid']:
1895 show
= self
.config
['tvdb_api'].series_by_sid(self
.config
['sid'])
1897 self
.config
['series_name']=show
[u
'seriesname']
1900 if self
.config
['series_name_override']:
1901 if self
.config
['series_name_override'].has_key(sid_or_name
.lower()):
1902 self
.config
['sid'] = (self
.config
['series_name_override'][sid_or_name
.lower()])
1903 show
= self
.config
['tvdb_api'].series_by_sid(self
.config
['sid'])
1905 self
.config
['series_name'] = show
[u
'seriesname']
1908 show
= self
.config
['tvdb_api'][sid_or_name
]
1910 self
.config
['series_name'] = show
[u
'seriesname']
1913 show
= self
.config
['tvdb_api'][sid_or_name
]
1915 self
.config
['series_name'] = show
[u
'seriesname']
1917 # end _searchforSeries
1919 def verifySeriesExists(self
):
1922 Series and Season or
1923 Series and Season and Episode number or
1924 Series and Episode name
1925 passed by the user exists on thetvdb.com
1926 return False and display an appropriate error if the TV data was not found
1927 return an tvdb_api instance of the TV show/season/episode data if it was found
1929 sid
=self
.config
['sid']
1930 series_name
=self
.config
['series_name']
1931 season
=self
.config
['season_num']
1932 episode
=self
.config
['episode_num']
1933 episode_name
=self
.config
['episode_name']
1935 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
))
1936 if episode_name
: # Find an exact match for the series and episode name
1939 seriesfound
=self
._searchforSeries
(sid
).search(episode_name
)
1941 seriesfound
=self
._searchforSeries
(series_name
).search(episode_name
)
1942 if len(seriesfound
) != 0:
1943 for ep
in seriesfound
:
1944 if ep
['seriesid'] == '999999999':
1945 self
.config
['sid'] = ep
['seriesid']
1947 if (ep
['episodename'].lower()).startswith(episode_name
.lower()):
1948 if len(ep
['episodename']) > (len(episode_name
)+1):
1949 # Skip episodes the are not part of a set of (1), (2) ... etc
1950 if ep
['episodename'][len(episode_name
):len(episode_name
)+2] != ' (':
1952 series_sid
= ep
['seriesid']
1953 self
.config
['sid'] = ep
['seriesid']
1954 self
.config
['season_num'] = ep
['seasonnumber']
1955 self
.config
['episode_num'] = ep
['episodenumber']
1958 series_sid
= ep
['seriesid']
1959 self
.config
['sid'] = ep
['seriesid']
1960 self
.config
['season_num'] = ep
['seasonnumber']
1961 self
.config
['episode_num'] = ep
['episodenumber']
1963 raise tvdb_episodenotfound
1964 # Search for the series or series & season or series & season & episode
1966 if episode
: # series & season & episode
1967 seriesfound
=self
._searchforSeries
(series_name
)[int(season
)][int(episode
)]
1968 if seriesfound
['seriesid'] == '999999999':
1970 self
.config
['sid'] = seriesfound
['seriesid']
1971 self
.config
['episode_name'] = seriesfound
['episodename']
1972 else: # series & season
1973 seriesfound
=self
._searchforSeries
(series_name
)[int(season
)]
1975 seriesfound
=self
._searchforSeries
(series_name
) # Series only
1976 except tvdb_shownotfound
:
1977 # No such show found.
1978 # Use the show-name from the files name, and None as the ep name
1980 sys
.stderr
.write(u
"\n! Warning: Series (%s) not found\n" % (
1984 sys
.stderr
.write(u
"\n! Warning: Series TVDB number (%s) not found\n" % (
1988 except (tvdb_seasonnotfound
, tvdb_episodenotfound
, tvdb_attributenotfound
):
1989 # The season, episode or name wasn't found, but the show was.
1990 # Use the corrected show-name, but no episode name.
1991 if series_name
== None:
1994 sys
.stderr
.write(u
"\n! Warning: For Series (%s), season (%s) or Episode (%s) not found \n"
1995 % (series_name
, season
, episode
)
1998 sys
.stderr
.write(u
"\n! Warning: For Series (%s), Episode (%s) not found \n"
1999 % (series_name
, episode_name
)
2002 sys
.stderr
.write(u
"\n! Warning: For Series (%s), season (%s) not found \n" % (
2003 series_name
, season
)
2006 except tvdb_error
, errormsg
:
2007 # Error communicating with thetvdb.com
2008 if sid
: # Maybe the 5 digit number was a series name (e.g. 90210)
2009 self
.config
['series_name']=self
.config
['sid']
2010 self
.config
['sid'] = None
2011 return self
.verifySeriesExists()
2013 u
"\n! Warning: Error contacting www.thetvdb.com:\n%s\n" % (errormsg
)
2016 except tvdb_userabort
, errormsg
:
2017 # User aborted selection (q or ^c)
2018 print "\n", errormsg
2022 # end verifySeriesExists
2024 def _resizeGraphic(self
, filename
, resize
):
2025 """Resize a graphics file
2026 return False and display an error message if the graphics resizing failed
2027 return True if the resize was succcessful
2029 if self
.config
['simulation']:
2031 u
"Simulation resize command (mogrify -resize %s %s)\n" % (resize
, filename
)
2034 if _useImageMagick('-resize %s "%s"' % (resize
, filename
)):
2036 u
'\n! Warning: Resizing failed command (mogrify -resize %s "%s")\n' % (resize
, filename
)
2040 # end _resizeGraphic
2042 def _downloadURL(self
, url
, OutputFileName
):
2043 """Download the specified graphic file from a URL
2044 return False if no file was downloaded
2045 return True if a file was successfully downloaded
2047 # Only download a file if it does not exist or the option overwrite is selected
2048 if not self
.config
['overwrite'] and os
.path
.isfile(OutputFileName
):
2051 if self
.config
['simulation']:
2053 u
"Simulation download of URL(%s) to File(%s)\n" % (url
, OutputFileName
)
2058 tmp_URL
= url
.replace("http://", "")
2059 url
= "http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
2062 dat
= urllib
.urlopen(url
).read()
2064 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
))
2068 target_socket
= open(OutputFileName
, "wb")
2069 target_socket
.write(dat
)
2070 target_socket
.close()
2072 sys
.stderr
.write( u
"\n! Warning: Download IOError for Filename(%s), may be the directory is invalid\nError:(%s)\n" % (OutputFileName
, e
))
2075 # Verify that the downloaded file was NOT HTML instead of the intended file
2077 p
= subprocess
.Popen(u
'file "%s"' % OutputFileName
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
2078 except Exception, e
:
2079 sys
.stderr
.write( u
"\n! Warning: Download Exception for Filename(%s)\nError:(%s)\n" % (OutputFileName
, e
))
2083 data
= p
.stdout
.readline()
2085 data
= data
.encode('utf8')
2086 except UnicodeDecodeError:
2087 data
= unicode(data
,'utf8')
2088 index
= data
.find(u
'HTML document text')
2092 os
.remove(OutputFileName
) # Delete the useless HTML text
2093 if self
.config
['mythtv_verbose']:
2094 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
))
2098 def _setGraphicsFileNameFormat(self
):
2099 """Return a file name format (e.g. seriesname - episode name.extention)
2100 return a filename format string
2102 if self
.config
['g_defaultname']:
2103 return u
'%(url)s.%(ext)s'
2105 cfile
['seriesid']=self
.config
['sid']
2106 cfile
['series'] = self
.sanitiseFileName(self
.config
['series_name'])
2107 if cfile
['series'] != self
.config
['series_name']:
2108 self
.config
['g_series'] = self
.config
['g_series'].replace(self
.config
['series_name'], cfile
['series'])
2109 if self
.config
['season_num']:
2110 cfile
['seasonnumber']=int(self
.config
['season_num'])
2112 cfile
['seasonnumber']=0
2113 if self
.config
['episode_num']:
2114 cfile
['episodenumber']=int(self
.config
['episode_num'])
2116 cfile
['episodenumber']=0
2117 cfile
['episodename']=self
.config
['episode_name']
2118 cfile
['seq']=u
'%(seq)02d'
2119 cfile
['ext']=u
'%(ext)s'
2121 if self
.config
['season_num']:
2122 return self
.config
['g_season'] % cfile
2124 return self
.config
['g_series'] % cfile
2125 # end _setGraphicsFileNameFormat
2127 def _downloadGraphics(self
, urls
, mythtv
=False):
2128 """Download graphic file(s) from a URL list (string of one or more URLs separated by a CR
2130 return None is the string of urls has no urls
2131 return False if the any of the urls are corrupt
2132 return file name of the LAST file downloaded (special for MythTV data base updates)
2134 global graphicsDirectories
2136 if urls
== None: return None
2137 if urls
== '': return None
2138 tmp_list
=urls
.split('\n')
2144 if not len(url_list
):
2145 return None # There were no URLs in the list
2149 self
.config
['log'].debug(u
'Checking for a key in (%s)' % (x
))
2153 u
"\n! Warning: URL list does not have a graphics type key(%s)\n" % (x
)
2156 if url_dict
.has_key(x
[:i
]):
2157 temp_array
= [x
[i
+1:],'']
2158 url_dict
[x
[:i
]].append(temp_array
)# Collect a list of the same graphics type of URLs
2159 else: # The first URL of a new graphics type. Also URL replacement code left in place just in case
2160 url_dict
[x
[:i
]]=[[(x
[i
+1:]).replace(u
"http://www.thetvdb.com",u
"http://www.thetvdb.com"),'']]
2162 unique_dir
={u'poster': ['posterdir', True], u'banner': ['bannerdir', True], u'fanart': ['fanartdir', True], u'filename': ['episodeimagedir', True]}
2163 # If a graphics directory was not specified then default to the 'allgraphics' directory
2164 if not self
.config
['posterdir']: self
.config
['posterdir'] = self
.config
['allgraphicsdir']
2165 if not self
.config
['bannerdir']: self
.config
['bannerdir'] = self
.config
['allgraphicsdir']
2166 if not self
.config
['fanartdir']: self
.config
['fanartdir'] = self
.config
['allgraphicsdir']
2167 if not self
.config
['episodeimagedir']: self
.config
['episodeimagedir'] = self
.config
['allgraphicsdir']
2169 # Check if any of the downloaded graphics will share the same directory
2170 for key
in unique_dir
.keys():
2171 for k
in unique_dir
.keys():
2173 if self
.config
[unique_dir
[key
][0]] == self
.config
[unique_dir
[k
][0]]:
2174 unique_dir
[key
][1] = False
2177 dirs
={u
'poster': self
.config
['posterdir'], u
'banner': self
.config
['bannerdir'],
2178 u
'fanart': self
.config
['fanartdir'], u
'filename': self
.config
['episodeimagedir']}
2180 # Figure out filenaming convention
2181 file_format
= self
._setGraphicsFileNameFormat
()
2183 # Set the graphics fully qualified filenames matched to a URL
2184 for URLtype
in url_dict
:
2186 if self
.absolutepath
:
2187 if URLtype
== 'poster':
2188 tmpgraphicdir
= graphicsDirectories
['coverfile']
2190 tmpgraphicdir
= graphicsDirectories
[URLtype
]
2191 if not len(self
.config
['localpaths'][tmpgraphicdir
]):
2194 directory
= self
.config
['localpaths'][tmpgraphicdir
][0]
2196 directory
= dirs
[URLtype
][0]
2198 directory
= dirs
[URLtype
]
2200 for url
in url_dict
[URLtype
]:
2201 (dirName
, fileName
) = os
.path
.split(url
[0])
2202 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
2203 fileBaseName
= self
.sanitiseFileName(fileBaseName
)
2204 # Fix file extentions in all caps or 4 character JPEG extentions
2205 fileExtension
= fileExtension
.lower()
2206 if fileExtension
== '.jpeg':
2207 fileExtension
= '.jpg'
2208 cfile
={u'url': fileBaseName, u'seq': seq_num, u'ext': fileExtension[1:]}
2209 if not isValidPosixFilename(self
.config
['series_name']):
2210 if file_format
.startswith(self
.config
['series_name']):
2211 file_format
= file_format
.replace(self
.config
['series_name'], self
.sanitiseFileName(self
.config
['series_name']))
2212 cfile
['series'] = self
.sanitiseFileName(self
.config
['series_name'])
2213 cfile
['seriesid'] = self
.config
['sid']
2215 if URLtype
!= 'filename':
2216 if unique_dir
[URLtype
][1]:
2217 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+file_format
% cfile
2220 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+file_format
% cfile
2222 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+URLtype
.capitalize()+' - '+file_format
% cfile
2224 if self
.config
['season_num']:
2225 cfile
['seasonnumber']=int(self
.config
['season_num'])
2227 cfile
['seasonnumber'] = 0
2228 if self
.config
['episode_num']:
2229 cfile
['episodenumber']=int(self
.config
['episode_num'])
2231 cfile
['episodenumber'] = 0
2232 cfile
['episodename'] = self
.sanitiseFileName(self
.config
['episode_name'])
2233 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+self
.config
['ep_metadata'] % cfile
2236 # Download the graphics and resize if requested - Ignore download or resize issues!
2237 failed_download
= False
2238 for URLtype
in url_dict
:
2240 for pairs
in url_dict
[URLtype
]:
2241 if self
._downloadURL
(pairs
[0], pairs
[1]):
2242 if URLtype
== u
'poster' and self
.config
['posterresize']:
2243 self
._resizeGraphic
(pairs
[1], self
.config
['posterresize'])
2244 elif URLtype
== u
'fanart' and self
.config
['fanartresize']:
2245 self
._resizeGraphic
(pairs
[1], self
.config
['fanartresize'])
2246 elif not os
.path
.isfile(pairs
[1]): # Check if the file already was downloaded
2247 failed_download
= True # The download failed
2248 if self
.config
['mythtv_verbose']:
2249 sys
.stderr
.write(u
'\nA graphics file failed to be downloaded. A file issue or a corrupt (HTML) file.(%s)\n' % pairs
[1])
2251 if self
.config
['maximum']: # Has the maximum number of graphics been downloaded?
2252 if seq_num
== int(self
.config
['maximum']):
2257 return pairs
[1] # The name of the LAST graphics successfully downloaded
2258 # end _downloadGraphics
2260 def getGraphics(self
, graphics_type
):
2261 """Retrieve Poster or Fan Art or Banner or Episode image graphics URL(s)
2262 return None if no graphics URLs were found
2263 return a string of URLs
2266 series_name
=self
.config
['series_name']
2267 season
=self
.config
['season_num']
2268 episode
=self
.config
['episode_num']
2269 episode_name
=self
.config
['episode_name']
2270 lang
=self
.config
['local_language']
2274 if self
.config
['sid']:
2275 URLs
= self
.config
['tvdb_api'].ttvdb_parseBanners(self
.config
['sid'])
2277 URLs
= self
.config
['tvdb_api'].ttvdb_parseBanners(self
.config
['tvdb_api']._nameToSid
(series_name
))
2278 except Exception, e
:
2281 if graphics_type
== self
.fanart_type
: # Series fanart graphics
2282 if not len(URLs
[u
'fanart']):
2284 for url
in URLs
[u
'fanart']:
2285 graphics
.append(url
)
2286 elif season
== None and episode
== None and episode_name
== None:
2287 if not len(URLs
[u
'series']):
2289 if graphics_type
== self
.banner_type
: # Series Banners
2290 for url
in URLs
[u
'series']:
2291 graphics
.append(url
)
2292 else: # Series Posters
2293 for url
in URLs
[u
'poster']:
2294 graphics
.append(url
)
2296 if not len(URLs
[u
'season']):
2298 if graphics_type
== self
.banner_type
: # Season Banners
2300 for url
in URLs
[u
'season']:
2301 if url
[u
'bannertype2'] == u
'seasonwide' and url
[u
'season'] == season
:
2302 season_banners
.append(url
)
2303 if not len(season_banners
):
2305 graphics
= season_banners
2306 else: # Season Posters
2308 for url
in URLs
[u
'season']:
2309 if url
[u
'bannertype2'] == u
'season' and url
[u
'season'] == season
:
2310 season_posters
.append(url
)
2311 if not len(season_posters
):
2313 graphics
= season_posters
2316 if self
.config
['nokeys'] and not self
.config
['download']:
2319 key_tag
=graphics_type
+u
':'
2322 wasanythingadded
= 0
2323 anyotherlanguagegraphics
=u
''
2324 englishlanguagegraphics
=u
''
2325 for URL
in graphics
:
2326 if graphics_type
== 'filename':
2327 if URL
[graphics_type
] == None:
2329 if lang
: # Is there a language to filter URLs on?
2330 if lang
== URL
['language']:
2331 if graphics_type
!= self
.ep_image_type
:
2332 graphicsURLs
+=key_tag
+URL
['_bannerpath']+'\n'
2334 graphicsURLs
+=key_tag
+URL
[graphics_type
]+'\n'
2335 else: # Check for fall back graphics in case there are no selected language graphics
2336 if u
'en' == URL
['language']:
2337 if graphics_type
!= self
.ep_image_type
:
2338 englishlanguagegraphics
+=key_tag
+URL
['_bannerpath']+'\n'
2340 englishlanguagegraphics
+=key_tag
+URL
[graphics_type
]+'\n'
2342 if graphics_type
!= self
.ep_image_type
:
2343 anyotherlanguagegraphics
+=key_tag
+URL
['_bannerpath']+'\n'
2345 anyotherlanguagegraphics
+=key_tag
+URL
[graphics_type
]+'\n'
2347 if graphics_type
!= self
.ep_image_type
:
2348 graphicsURLs
+=key_tag
+URL
['_bannerpath']+'\n'
2350 graphicsURLs
+=key_tag
+URL
[graphics_type
]+'\n'
2351 if wasanythingadded
== len(graphicsURLs
):
2353 wasanythingadded
= len(graphicsURLs
)
2355 if self
.config
['maximum']: # Has the maximum number of graphics been downloaded?
2356 if count
== int(self
.config
['maximum']):
2359 if not len(graphicsURLs
):
2360 if len(englishlanguagegraphics
): # Fall back to English graphics
2361 graphicsURLs
= englishlanguagegraphics
2362 elif len(anyotherlanguagegraphics
): # Fall back-back to any available graphics
2363 graphicsURLs
= anyotherlanguagegraphics
2365 if self
.config
['debug_enabled']:
2366 print "\nGraphics:\n", graphicsURLs
2368 if not len(graphicsURLs
): # Are there any graphics?
2371 if len(graphicsURLs
) == 1 and graphicsURLs
[0] == graphics_type
+':':
2372 return None # Due to the language filter there may not be any URLs
2374 return(graphicsURLs
)
2377 def getTopRatedGraphics(self
, graphics_type
):
2378 """Retrieve only the top rated series Poster, Fan Art and Banner graphics URL(s)
2379 return None if no top rated graphics URLs were found
2380 return a string of top rated URLs
2382 if graphics_type
== u
'filename':
2383 self
.config
['log'].debug(u
'! There are no such thing as top rated Episode image URLs')
2386 series_name
=self
.config
['series_name']
2387 keys
=self
.config
['nokeys']
2388 if self
._searchforSeries
(series_name
)[graphics_type
] != None:
2389 if keys
and not self
.config
['download']:
2390 toprated
=(self
._searchforSeries
(series_name
)[graphics_type
])+'\n'
2392 toprated
=(u
'%s:%s\n' % (graphics_type
, self
._searchforSeries
(series_name
)[graphics_type
]))
2394 # end getTopRatedGraphics
2396 def _downloadEpisodeData(self
,ep_data
):
2397 """Down load episode meta data and episode image graphics
2398 return True whether or not there was episode data processed
2400 if not len(ep_data
):
2401 return True # There were no episode data in the list
2402 ep_data_list
=[] # An array of episode meta data
2404 first_key
=self
.config
['ep_include_data'][0]+':'
2405 key_size
=len(first_key
)
2407 while len(ep_data
): # Grab each episode's set of meta data
2409 self
.config
['log'].debug(u
'Parse out the episode data from an episode meta dats string')
2410 end
= ep_data
[key_size
:].index(first_key
)
2411 ep_data_list
.append(ep_data
[:end
+key_size
])
2412 ep_data
=ep_data
[end
+key_size
:]
2414 ep_data_list
.append(ep_data
)
2417 if not self
.config
['metadatadir']:
2418 self
.config
['metadatadir'] = os
.getcwd()
2420 # Process each episode's meta data
2421 for episode
in ep_data_list
:
2422 tmp_data
= episode
.split('\n')
2423 for i
in range(len(tmp_data
)):
2424 tmp_data
[i
] = tmp_data
[i
].rstrip()# Remove \n characters from the end of each record
2426 for data
in tmp_data
:
2428 self
.config
['log'].debug(u
'Checking for key in episode meta data')
2429 tmp_dict
[data
[:data
.index(':')]] = data
[data
.index(':')+1:]
2432 tmp_dict
['ext']='meta'
2434 for key
in ['seasonnumber', 'episodenumber']:
2435 if tmp_dict
.has_key(key
):
2436 tmp_dict
[key
] = int(tmp_dict
[key
])
2437 if not tmp_dict
.has_key(u
'episodename'):
2438 tmp_dict
[u
'episodename'] = u
''
2439 filename
="%s/%s" % (self
.config
['metadatadir'],self
.config
['ep_metadata'] % tmp_dict
)
2440 image_filename
= None
2441 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2442 url
= tmp_dict
['filename']
2444 if not self
.config
['episodeimagedir']:
2445 self
.config
['episodeimagedir'] = self
.config
['allgraphicsdir']
2446 (dirName
, fileName
) = os
.path
.split(url
)
2447 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
2448 tmp_dict
[u
'ext']=fileExtension
[1:]
2449 image_filename
= "%s/%s" % (self
.config
['episodeimagedir'], self
.config
['ep_metadata'] % tmp_dict
)
2450 # Only download a file if it does not exist or the option overwrite is selected
2451 # or the option update is selected and the local meta data file is
2452 # older than the episode data on thetvdb.com wiki
2454 if self
.config
['update'] and tmp_dict
.has_key('lastupdated') and os
.path
.isfile(filename
):
2455 if int(tmp_dict
['lastupdated']) > int(os
.path
.getmtime(filename
)):
2458 if not self
.config
['overwrite'] and not outofdate
:
2459 if self
.config
['get_ep_meta'] and self
.config
['get_ep_image']:
2461 if os
.path
.isfile(filename
) and os
.path
.isfile(image_filename
):
2464 if os
.path
.isfile(filename
):
2466 elif self
.config
['get_ep_meta']:
2467 if os
.path
.isfile(filename
):
2469 elif self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2470 url
= tmp_dict
['filename']
2472 if os
.path
.isfile(image_filename
):
2479 if self
.config
['simulation']:
2480 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2481 self
.config
['log'].debug(u
'Simulate downloading an episode image')
2482 url
= tmp_dict
['filename']
2484 sys
.stdout
.write(u
"Simulation create episode image file(%s)\n" % image_filename
)
2485 if self
.config
['get_ep_meta']:
2487 u
"Simulation create meta data file(%s)\n" % filename
2491 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2492 if tmp_dict
['filename'] != 'None':
2493 self
._downloadGraphics
('filename:'+tmp_dict
['filename'])
2495 # Write out an episode meta data file
2496 if self
.config
['get_ep_meta']:
2497 fHandle
= codecs
.open(filename
, 'w', 'utf8')
2498 fHandle
.write(episode
)
2502 # end _downloadEpisodeData
2504 def _changeToCommas(self
,meta_data
):
2505 """Remove '|' and replace with commas
2506 return the modified text
2508 if not meta_data
: return meta_data
2509 meta_data
= (u
'|'.join([d
for d
in meta_data
.split('| ') if d
]))
2510 return (u
', '.join([d
for d
in meta_data
.split(u
'|') if d
]))
2511 # end _changeToCommas
2513 def _changeAmp(self
, text
):
2514 """Change & values to ASCII equivalents
2515 return the modified text
2517 if not text
: return text
2518 text
= text
.replace(""", u
"'").replace("\r\n", u
" ")
2519 text
= text
.replace(r
"\'", u
"'")
2523 def getSeriesEpisodeData(self
):
2524 """Get Series Episode meta data. This can be one specific episode or all of a seasons episodes
2525 or all episodes for an entire series.
2526 return an empy sting of no episode meta data was found
2527 reurn a string containing key value pairs of episode meta data
2529 sid
=self
.config
['sid']
2530 series_name
=self
.config
['series_name']
2531 season_num
=self
.config
['season_num']
2532 episode_num
=self
.config
['episode_num']
2533 episode_name
=self
.config
['episode_name']
2539 tmp_cast
= self
._searchforSeries
(series_name
)[u
'_actors']
2544 for cast
in tmp_cast
:
2545 cast_members
+=(cast
['name']+u
', ').encode('utf8')
2546 if cast_members
!= '':
2548 cast_members
= cast_members
[:-2].encode('utf8')
2549 except UnicodeDecodeError:
2550 cast_members
= unicode(cast_members
[:-2],'utf8')
2551 cast_members
= self
._changeAmp
(cast_members
)
2552 cast_members
= self
._changeToCommas
(cast_members
)
2553 cast_members
=cast_members
.replace('\n',' ')
2558 genres_string
= self
._searchforSeries
(series_name
)[u
'genre'].encode('utf8')
2561 if genres_string
!= None and genres_string
!= '':
2562 genres
= self
._changeAmp
(genres_string
)
2563 genres
= self
._changeToCommas
(genres
)
2565 seasons
=self
._searchforSeries
(series_name
).keys() # Get the seasons for this series
2566 episodes_metadata
=u
''
2567 for season
in seasons
:
2568 if season_num
: # If a season was specified skip other seasons
2569 if season
!= int(season_num
):
2571 episodes
=self
._searchforSeries
(series_name
)[season
].keys()# Get the episodes for this season
2572 for episode
in episodes
: # If an episode was specified skip other episodes
2574 if episode
!= int(episode_num
):
2577 if sid
: # Ouput the full series name
2579 ep_data
["series"]=self
._searchforSeries
(sid
)[u
'seriesname'].encode('utf8')
2580 except AttributeError:
2584 ep_data
["series"]=self
._searchforSeries
(series_name
)[u
'seriesname'].encode('utf8')
2585 except AttributeError:
2587 available_keys
=self
._searchforSeries
(series_name
)[season
][episode
].keys()
2589 ep_data
[u
'gueststars']=''
2590 for key
in available_keys
:
2591 if self
._searchforSeries
(series_name
)[season
][episode
][key
] == None:
2594 text
= self
._searchforSeries
(series_name
)[season
][episode
][key
]
2595 text
= self
._changeAmp
(text
)
2596 text
= self
._changeToCommas
(text
)
2597 ep_data
[key
.lower()]=text
.replace('\n',' ')
2598 for key
in self
.config
['ep_include_data']: # Select and sort the required meta data
2599 if ep_data
.has_key(key
):
2600 if key
== u
'gueststars':
2601 if ep_data
[key
] == '':
2602 tmp
+=u
'Cast:%s\n' % cast_members
2604 if (len(ep_data
[key
]) > 128) and not ep_data
[key
].count(','):
2605 tmp
+=u
'Cast:%s\n' % cast_members
2607 tmp
+=u
'Cast:%s, %s\n' % (cast_members
, ep_data
[key
])
2610 tmp
+=u
'%s:%s\n' % (key
, ep_data
[key
])
2611 except UnicodeDecodeError:
2612 tmp
+=u
'%s:%s\n' % (key
, unicode(ep_data
[key
], "utf8"))
2613 tmp
+=u
'Runtime:%s\n' % self
._searchforSeries
(series_name
)[u
'runtime']
2615 tmp
+=u
'Genres:%s\n' % genres
2617 episodes_metadata
+=tmp
2618 return episodes_metadata
2619 # end Getseries_episode_data
2621 def returnFilename(self
):
2622 """Return a single file name (excluding file extension and directory), limited by the current
2623 variables (sid, season name, season number ... etc). Typically used when writing a meta file
2624 or naming/renaming a video file after a TV show recording.
2625 return False and out put an error if there not either a series id (SID) or series name
2626 return False and out put an error if there proper episode information (numbers or name)
2627 return False if the option (-MGF) used and there is not exact TV series name match
2628 return a specific episode filename
2630 sid
=self
.config
['sid']
2631 series_name
=self
.config
['series_name']
2632 season_num
=self
.config
['season_num']
2633 episode_num
=self
.config
['episode_num']
2634 episode_name
=self
.config
['episode_name']
2636 if not sid
and not series_name
:
2638 u
"\n! Warning: There must be at least series name or SID to request a filename\n"
2642 if season_num
and episode_num
:
2644 elif not episode_name
:
2646 u
'\n! Error: There must be at least "season and episode numbers" or "episode name" to request a filename\n'
2650 # Special logic must be used if the (-MG) guessing option has been requested
2651 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
2653 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
2654 except Exception, e
:
2655 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
))
2657 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
, self
.config
['series_name'].lower()):
2658 self
.config
['sid'] = allmatchingseries
['sid']
2659 self
.config
['series_name'] = allmatchingseries
['name']
2661 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'])
2664 episode
= self
.verifySeriesExists()
2666 if not episode
: # Make sure an episode was found
2668 u
'\n! Error: The episode was not found for series(%s), Episode name(%s)\n' % (series_name
, episode_name
)
2672 sid
=self
.config
['sid']
2674 if UI_selectedtitle
and (self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']):
2675 self
.config
['series_name'] = UI_selectedtitle
2677 series_name
=self
.config
['series_name']
2678 season_num
=self
.config
['season_num']
2679 episode_num
=self
.config
['episode_num']
2680 episode_name
=self
.config
['episode_name']
2682 tmp_dict
={'series': series_name, 'seasonnumber': season_num, 'episodenumber': episode_num, 'episodename': episode_name, 'sid': sid }
2685 for key
in ['seasonnumber', 'episodenumber']:
2686 if tmp_dict
.has_key(key
):
2687 tmp_dict
[key
] = int(tmp_dict
[key
])
2689 return self
.sanitiseFileName(u
"%s" % (self
.config
['ep_metadata'] % tmp_dict
)[:-1])
2690 # end returnFilename
2692 def processTVdatabaseRequests(self
):
2693 """Process the data/download requests as indicated by the variables
2694 return None if the series/season/episode does not exist
2695 return None if there is no data to process for the request actions
2696 return a string for display or further processing that satisfies the reqested actions
2698 if self
.verifySeriesExists():# Getting a filename is a single event nothing else is returned
2699 if self
.config
['ret_filename']:
2700 return self
.returnFilename()
2704 types
={'get_fanart': self.fanart_type, 'get_poster': self.poster_type, 'get_banner': self.banner_type}
2705 if self
.config
['toprated']:
2706 typegetGraphics
=self
.getTopRatedGraphics
2708 typegetGraphics
=self
.getGraphics
2710 if self
.verifySeriesExists():
2711 if self
.config
['download']: # Deal only with graphics display or downloads
2712 for key
in types
.keys():
2713 if key
== 'get_ep_image': # Ep image downloads processed below
2715 if self
.config
[key
]:
2716 if self
._downloadGraphics
(typegetGraphics(types
[key
])):
2718 u
"%s downloading successfully processed\n" % key
.title()
2722 for key
in types
.keys():
2723 if self
.config
[key
]:
2724 string
=typegetGraphics(types
[key
])
2727 if url_string
!= '':
2728 results
+=url_string
# Add graphic URLs to returned results
2730 # Should episode meta data or episode image be processed?
2731 if self
.config
['get_ep_meta'] or self
.config
['get_ep_image']:
2732 if self
.config
['download']: # Deal only with episode data display or download
2733 if self
._downloadEpisodeData
(self
.getSeriesEpisodeData()):
2735 u
"Episode meta data and/or images downloads successfully processed\n"
2738 eps_string
= self
.getSeriesEpisodeData()
2739 if eps_string
!= '':
2740 results
+=eps_string
# Add episode meta data to returned results
2745 if results
[len(results
)-1] == '\n':
2746 return results
[:len(results
)-1]
2751 # end processTVdatabaseRequests
2753 def __repr__(self
): # Just a place holder
2760 class VideoFiles(Tvdatabase
):
2761 """Process all video file and/or directories containing video files. These TV Series video
2762 files must be named so that a "series name or sid" and/or "season and episode number"
2763 can be extracted from the video file name. It is best to have renamed the TV series video files with
2764 tvnamer before using these files with jamu. Any video file without season and episode numbers is
2765 assumed to be a movie. Files that do not match the previously described criterion will be skipped.
2766 tvnamer can be found at:
2767 http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search
2769 def __init__(self
, configuration
):
2770 """Retrieve the configuration options
2772 super(VideoFiles
, self
).__init
__(configuration
)
2775 image_extensions
= ["png", "jpg", "bmp"]
2777 def _findFiles(self
, args
, recursive
= False, verbose
= False):
2779 Takes a file name or folder path and grabs files inside them. Does not recurse
2780 more than one level (if a folder is supplied, it will list files within),
2781 unless recurse is True, in which case it will recursively find all files.
2782 return an array of file names
2786 for cfile
in args
: # Directories must exist and be both readable and writable
2787 if os
.path
.isdir(cfile
) and not os
.access(cfile
, os
.F_OK | os
.R_OK
):
2788 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
))
2791 if os
.path
.isdir(cfile
):
2792 # ramsi allow regex in ignore-directory
2793 for directory
in self
.config
['ignore-directory']: # ignore directory list
2794 #if not cfile.startswith(directory):
2795 if re
.search(directory
,cfile
) is None:
2799 if ignore
: # Skip this directory
2801 if os
.path
.isdir(cfile
):
2802 index
= cfile
.find(u
'VIDEO_TS')
2804 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
))
2807 cfile
= unicode(cfile
, u
'utf8')
2808 except (UnicodeEncodeError, TypeError):
2810 for sf
in os
.listdir(cfile
):
2812 newpath
= os
.path
.join(cfile
, sf
)
2814 sys
.stderr
.write(u
"\n! Error: This video file cannot be processed skipping:\n")
2815 sys
.stderr
.write(sf
)
2816 sys
.stderr
.write(u
"\nIt may be advisable to rename this file and try again.\n\n")
2818 if os
.path
.isfile(newpath
):
2819 allfiles
.append(newpath
)
2823 self
._findFiles
([newpath
], recursive
= recursive
, verbose
= verbose
)
2828 elif self
.config
[u
'file_move_flag'] and not os
.access(cfile
, os
.F_OK | os
.R_OK | os
.W_OK
):
2829 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
))
2830 elif os
.path
.isfile(cfile
) and os
.access(cfile
, os
.F_OK | os
.R_OK
):
2831 allfiles
.append(cfile
) # Files must exist and be at least readable
2838 def _processNames(self
, names
, verbose
=False, movies
=False):
2840 Takes list of names, runs them though the self.config['name_parse'] regex parsing strings
2841 to extract series name, season and episode numbers. Non-video files are skipped.
2842 return an array of dictionaries containing series name, season and episode numbers, file path and full filename and file extention.
2846 filepath
, filename
= os
.path
.split( f
)
2847 filename
, ext
= os
.path
.splitext( filename
)
2849 # Remove leading . from extension
2850 ext
= ext
.replace(u
".", u
"", 1)
2851 self
.config
['log'].debug(u
'Checking for a valid video filename extension')
2852 if not ext
.lower() in self
.config
[u
'video_file_exts']:
2853 for key
in self
.image_extensions
:
2857 sys
.stderr
.write(u
"\n! Warning: Skipping non-video file name: (%s)\n" % (f
))
2861 for r
in self
.config
['name_parse']:
2862 match
= r
.match(filename
)
2864 # If the filename does not match the default regular
2865 # expressions, try to match the file path + filename with the
2866 # extended fullpath regular expression so we can extract the
2867 # needed information out of the pathname
2869 for r
in self
.config
['fullname_parse']:
2870 match
= r
.match(os
.path
.join(filepath
, filename
))
2875 self
.config
['log'].debug(u
'matched reg:%s'%match
.re
.pattern
)
2876 seriesname
, seasno
, epno
= match
.groups()
2878 #remove ._- characters from name (- removed only if next to end of line)
2879 seriesname
= re
.sub("[\._]|\-(?=$)", " ", seriesname
).strip()
2880 # ramsi remove [en] tags
2881 seriesname
= re
.sub("(?:\[.*\])+", " ", seriesname
).strip()
2883 seasno
, epno
= int(seasno
), int(epno
)
2885 if self
.config
['series_name_override']:
2886 if self
.config
['series_name_override'].has_key(seriesname
.lower()):
2887 if len((self
.config
['series_name_override'][seriesname
.lower()]).strip()) == 7:
2888 categories
+=u
', Movie'
2890 if movie
.endswith(self
.config
['hd_dvd']):
2891 movie
= movie
.replace(self
.config
['hd_dvd'], '')
2892 categories
+=u
', DVD'
2895 if movie
.endswith(self
.config
['dvd']):
2896 movie
= movie
.replace(self
.config
['dvd'], '')
2897 categories
+=u
', DVD'
2898 movie
= re
.sub("[\._]|\-(?=$)", " ", movie
).strip()
2899 # ramsi remove [en] tags
2900 movie
= re
.sub("(?:\[.*\])+", " ", movie
).strip()
2902 allEps
.append({ 'file_seriesname':movie
,
2905 'filepath':filepath
,
2906 'filename':filename
,
2908 'categories': categories
2910 except UnicodeDecodeError:
2911 allEps
.append({ 'file_seriesname':unicode(movie
,'utf8'),
2914 'filepath':unicode(filepath
,'utf8'),
2915 'filename':unicode(filename
,'utf8'),
2916 'ext':unicode(ext
,'utf8'),
2917 'categories': categories
2920 categories
+=u
', TV Series'
2922 allEps
.append({ 'file_seriesname':seriesname
,
2925 'filepath':filepath
,
2926 'filename':filename
,
2928 'categories': categories
2930 except UnicodeDecodeError:
2931 allEps
.append({ 'file_seriesname':unicode(seriesname
,'utf8'),
2934 'filepath':unicode(filepath
,'utf8'),
2935 'filename':unicode(filename
,'utf8'),
2936 'ext':unicode(ext
,'utf8'),
2937 'categories': categories
2940 if movies
: # Account for " - On DVD" and " HD - On DVD" extra text on file names
2941 categories
+=u
', Movie'
2944 if movie
.endswith(self
.config
['hd_dvd']):
2945 movie
= movie
.replace(self
.config
['hd_dvd'], '')
2946 categories
+=u
', DVD'
2949 if movie
.endswith(self
.config
['dvd']):
2950 movie
= movie
.replace(self
.config
['dvd'], '')
2951 categories
+=u
', DVD'
2952 movie
= re
.sub("[\._]|\-(?=$)", " ", movie
).strip()
2953 # ramsi remove [en] tags
2954 movie
= re
.sub("(?:\[.*\])+", " ", movie
).strip()
2956 allEps
.append({ 'file_seriesname':movie
,
2959 'filepath':filepath
,
2960 'filename':filename
,
2962 'categories': categories
2964 except UnicodeDecodeError:
2965 allEps
.append({ 'file_seriesname':unicode(movie
,'utf8'),
2968 'filepath':unicode(filepath
,'utf8'),
2969 'filename':unicode(filename
,'utf8'),
2970 'ext':unicode(ext
,'utf8'),
2971 'categories': categories
2974 sys
.stderr
.write(u
"\n! Warning: Skipping invalid name: %s\n" % (f
))
2982 def processFileOrDirectory(self
):
2983 '''This routine is NOT used for MythTV meta data processing.
2984 If directory path has been specified then create a list of files that qualify as video
2985 files / including recursed directories.
2986 Then parse the list of file names to determine (series, season number, ep number and ep name).
2987 Skip any video file that cannot be parsed for sufficient info.
2988 Loop through the list:
2989 > Check if the series, season, ... exists, skip with debug message if none found
2990 > Set variable with proper info: sid, series, season and episode numbers
2991 > Process the file's information per the variable to get graphics and or meta data
2992 return False and an error message and exist the script if there are no video files to process
2993 return None when all processing was complete
2994 return a string of file names if the "Filename" process option was True
2997 allFiles
= self
._findFiles
(self
.config
['video_dir'], self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
2998 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'])
3000 if len(validFiles
) == 0:
3001 sys
.stderr
.write(u
"\n! Error: No valid video files found\n")
3004 path_flag
= self
.config
['metadatadir']
3005 for cfile
in validFiles
:
3006 sys
.stdout
.write(u
"# Processing %(file_seriesname)s (season: %(seasno)d, episode %(epno)d)\n" % (cfile
))
3007 self
.config
['sid']=None
3008 self
.config
['episode_name'] = None
3009 self
.config
['series_name']=cfile
['file_seriesname']
3010 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3011 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3012 if not path_flag
: # If no metaddata directory specified then default to the video file dir
3013 self
.config
['metadatadir'] = cfile
['filepath']
3014 if self
.verifySeriesExists():
3015 self
.config
['log'].debug(u
"Found series(%s) season(%s) episode(%s)" % (self
.config
['series_name'], self
.config
['season_num'], self
.config
['episode_num']))
3016 if self
.config
['ret_filename']:
3017 returned
= self
.processTVdatabaseRequests()
3018 if returned
!= None and returned
!= False:
3019 filenames
+=returned
+'\n'
3021 self
.processTVdatabaseRequests()
3023 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']))
3024 self
.config
['log'].debug("# Done")
3025 if len(filenames
) == 0:
3028 return filenames
[:-1] # drop the last '\n'
3029 # end processFileOrDirectory
3031 def __repr__(self
): # Just a place holder
3038 class MythTvMetaData(VideoFiles
):
3039 """Process all mythvideo video files, update the video files associated MythTV meta data.
3040 Download graphics for those video files from either thetvdb.com or themovie.com. Video file names
3041 for TV episodes must series name, season and episode numbers. The video file's movie name must be
3042 an exact match with a movie title in themoviedb.com or the MythTV database must have an entry for
3043 the video file with a TMDB or an IMDB number (db field 'intref').
3045 def __init__(self
, configuration
):
3046 """Retrieve the configuration options
3048 super(MythTvMetaData
, self
).__init
__(configuration
)
3052 # A dictionary of meta data keys and initialized values
3053 global graphicsDirectories
3054 movie_file_format
=u
"%s/%s.%s"
3055 graphic_suffix
= {u'coverfile': u'_coverart', u'fanart': u'_fanart', u'banner': u'_banner'}
3056 graphic_name_suffix
= u
"%s/%s%s.%s"
3057 graphic_name_season_suffix
= u
"%s/%s Season %d%s.%s"
3060 def _getSubtitle(self
, cfile
):
3061 '''Get the MythTV subtitle (episode name)
3063 return episode name string
3065 self
.config
['sid']=None
3066 self
.config
['episode_name'] = None
3067 self
.config
['series_name']=cfile
['file_seriesname']
3068 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3069 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3070 self
.verifySeriesExists()
3071 return self
.config
['episode_name']
3075 def hashFile(self
, name
):
3076 '''Create metadata hash values for mythvideo files
3078 return u'' if the was an error with the video file or the video file length was zero bytes
3080 filename
= self
.rtnRelativePath(name
, u
'mythvideo')
3081 # Use the MythVideo hashing protocol when the video is in a storage groups
3082 if filename
[0] != u
'/':
3083 hash_value
= mythbeconn
.getHash(filename
, u
'Videos')
3084 if hash_value
== u
'NULL':
3089 # Use a local hashing routine when video is not in a Videos storage group
3090 # For original code: http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#Python
3092 longlongformat
= 'q' # long long
3093 bytesize
= struct
.calcsize(longlongformat
)
3094 f
= open(name
, "rb")
3095 filesize
= os
.path
.getsize(name
)
3097 if filesize
< 65536 * 2: # Video file is too small
3099 for x
in range(65536/bytesize
):
3100 buffer = f
.read(bytesize
)
3101 (l_value
,)= struct
.unpack(longlongformat
, buffer)
3103 hash = hash & 0xFFFFFFFFFFFFFFFF #to remain as 64bit number
3104 f
.seek(max(0,filesize
-65536),0)
3105 for x
in range(65536/bytesize
):
3106 buffer = f
.read(bytesize
)
3107 (l_value
,)= struct
.unpack(longlongformat
, buffer)
3109 hash = hash & 0xFFFFFFFFFFFFFFFF
3111 returnedhash
= "%016x" % hash
3114 except(IOError): # Accessing to this video file caused and error
3118 def rtnRelativePath(self
, abpath
, filetype
):
3119 '''Check if there is a Storage Group for the file type (video, coverfile, banner, fanart, screenshot)
3120 and form an apprioriate relative path and file name.
3121 return a relative path and file name
3122 return an absolute path and file name if there is no storage group for the file type
3127 # There is a chance that this is already a relative path or there is no Storage group for file type
3128 if not len(storagegroups
):
3130 if not storagegroups
.has_key(filetype
) or abpath
[0] != '/':
3133 # The file must already be in one of the directories specified by the file type's storage group
3134 for directory
in storagegroups
[filetype
]:
3135 if abpath
.startswith(directory
):
3136 return abpath
[len(directory
)+1:]
3139 # end rtnRelativePath
3141 def rtnAbsolutePath(self
, relpath
, filetype
):
3142 '''Check if there is a Storage Group for the file type (mythvideo, coverfile, banner, fanart,
3143 screenshot) and form an appropriate absolute path and file name.
3144 return an absolute path and file name
3145 return the relpath sting if the file does not actually exist in the absolute path location
3147 if relpath
== None or relpath
== u
'':
3150 # There is a chance that this is already an absolute path
3151 if relpath
[0] == u
'/':
3154 if self
.absolutepath
:
3155 if not len(self
.config
['localpaths'][filetype
]):
3157 directories
= self
.config
['localpaths'][filetype
]
3159 directories
= self
.config
[filetype
]
3161 for directory
in directories
:
3162 abpath
= u
"%s/%s" % (directory
, relpath
)
3163 if os
.path
.isfile(abpath
): # The file must actually exist locally
3166 return relpath
# The relative path does not exist at all the metadata entry is useless
3167 # end rtnAbsolutePath
3170 def removeCommonWords(self
, title
):
3171 '''Remove common words from a title
3172 return title striped of common words
3176 wordList
= [u
'the ', u
'a ', u
' '] # common word list. Leave double space as the last value.
3177 title
= title
.lower()
3178 for word
in wordList
:
3179 title
= title
.replace(word
, u
'')
3182 return filter(is_not_punct_char
, title
.strip())
3183 # end removeCommonWords()
3186 def _getTmdbIMDB(self
, title
, watched
=False, IMDB
=False, rtnyear
=False):
3187 '''Find and exact match of the movie name with what's on themoviedb.com
3188 If IMDB is True return an imdb#
3189 If rtnyear is True return IMDB# and the movie year in a dictionary
3190 return False (no matching movie found)
3191 return imdb# and/or tmdb#
3193 global video_type
, UI_title
3194 UI_title
= title
.replace(self
.config
[u
'hd_dvd'], u
'')
3195 UI_title
= UI_title
.replace(self
.config
[u
'dvd'], u
'')
3197 if UI_title
[-1:] == ')': # Get rid of the (XXXX) year from the movie title
3198 tmp_title
= UI_title
[:-7].lower()
3200 tmp_title
= UI_title
.lower()
3202 if self
.config
['series_name_override']:
3203 if self
.config
['series_name_override'].has_key(tmp_title
):
3204 return (self
.config
['series_name_override'][tmp_title
]).strip()
3213 results
= [self
.config
['tmdb_api'].searchIMDB(IMDB
)]
3215 results
= self
.config
['tmdb_api'].searchTMDB(user_tmdb
)
3217 if results
.has_key('releasedate'):
3218 return {'name': "%s (%s)" % (results['title'], results['releasedate'][:4]), u'sid': results[u'inetref']}
3220 return {'name': "%s" % (results['title'], ), u'sid': results[u'inetref']}
3222 return results
['inetref']
3224 results
= self
.config
['tmdb_api'].searchTitle(tmp_title
)
3225 except TmdbMovieOrPersonNotFound
, e
:
3227 except Exception, errormsg
:
3228 self
._displayMessage
(u
"themoviedb.com error for Movie(%s) invalid data error (%s)" % (title
, errormsg
))
3231 self
._displayMessage
(u
"themoviedb.com error for Movie(%s)" % title
)
3234 # Check if user's interactive response (Skip, selection, input #)
3235 if len(results
[0]) and self
.config
['interactive']:
3236 if results
[0].has_key('userResponse'):
3237 # Check if the user selected a specific movie from the list
3238 if results
[0]['userResponse'] == 'User selected':
3240 if results
[0].has_key('released'):
3241 data
= {'name': "%s (%s)" % (results[0]['name'], results[0]['released'][:4]), u'sid': results[0][u'id']}
3243 data
= {'name': "%s" % (results[0]['name'], ), u'sid': results[0][u'id']}
3246 return results
[0]['id']
3247 # Check if the user has entered a TMDB number themselves
3248 if results
[0]['userResponse'] == 'User input':
3249 user_tmdb
= results
[0]['id']
3251 # Check if the user wants this video to be ignored by Jamu from now on
3252 if results
[0]['id'] == '99999999':
3256 return results
[0]['id']
3259 if IMDB
: # This is required to allow graphic file searching both by a TMDB and IMDB numbers
3261 if results
[0].has_key('imdb_id'):
3262 return results
[0]['imdb_id'][2:]
3268 if UI_title
[-1:] == ')':
3269 name
= UI_title
[:-7].lower() # Just the movie title
3270 year
= UI_title
[-5:-1] # The movie release year
3272 name
= tmp_title
.lower()
3274 name
= name
.strip().replace(' ', ' ')
3277 for movie
in results
:
3278 if self
.removeCommonWords(movie
['name']) == self
.removeCommonWords(name
):
3280 if movie
.has_key('released'):
3281 TMDB_movies
.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3283 TMDB_movies
.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}
)
3285 if movie
.has_key(u
'released'):
3286 if movie
['released'][:4] == year
:
3288 return {'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
3291 TMDB_movies
.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3294 TMDB_movies
.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}
)
3296 elif movie
.has_key('alternative_name'):
3297 if self
.removeCommonWords(movie
['alternative_name']) == self
.removeCommonWords(name
):
3299 if movie
.has_key('released'):
3300 TMDB_movies
.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3302 TMDB_movies
.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}
)
3304 if movie
.has_key(u
'released'):
3305 if movie
['released'][:4] == year
:
3307 return {'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
3310 TMDB_movies
.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3313 TMDB_movies
.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}
)
3316 # When there is only one match but NO year to confirm then it is OK to assume an exact match
3317 if len(TMDB_movies
) == 1 and year
== '':
3319 return TMDB_movies
[0]
3321 return TMDB_movies
[0][u
'sid']
3323 if imdb_lib
: # Can a imdb.com search be done?
3324 imdb_access
= imdb
.IMDb()
3327 movies_found
= imdb_access
.search_movie(tmp_title
.encode("ascii", 'ignore'))
3330 if not len(movies_found
):
3333 for movie
in movies_found
: # Get rid of duplicates
3334 try: # Protect against bad data from IMDBpy
3335 if movie
.has_key('year'):
3336 temp
= {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])}
3338 temp
= {imdb_access.get_imdbID(movie): movie['title']}
3341 if tmp_movies
.has_key(temp
.keys()[0]):
3343 tmp_movies
[temp
.keys()[0]] = temp
[temp
.keys()[0]]
3344 for movie
in tmp_movies
:
3345 if tmp_movies
[movie
][:-7].lower() == name
or self
.removeCommonWords(tmp_movies
[movie
][:-7]) == self
.removeCommonWords(name
):
3347 if tmp_movies
[movie
][-5:-1] == year
:
3349 return {'name': tmp_movies[movie], u'sid': movie}
3351 return u
"%07d" % int(movie
) # Pad out IMDB# with leading zeroes
3352 IMDB_movies
.append({'name': tmp_movies[movie], u'sid': movie}
)
3354 if len(IMDB_movies
) == 1: # If this is the only choice and titles matched then auto pick it
3355 if self
.removeCommonWords(IMDB_movies
[0]['name'][:-7]) == self
.removeCommonWords(name
):
3357 return IMDB_movies
[0]
3359 return u
"%07d" % int(IMDB_movies
[0][u
'sid'])
3361 # Does IMDB list this movie?
3362 if len(IMDB_movies
) == 0:
3365 # Did the user want an interactive interface?
3366 if not self
.config
['interactive']:
3369 # Force only an IMDB look up for a movie
3370 movies
= IMDB_movies
3373 ui
= jamu_ConsoleUI(config
= self
.config
, log
= self
.config
['log'])
3375 inetref
= ui
.selectSeries(movies
)
3376 except tvdb_userabort
:
3377 if video_type
==u
'IMDB' or len(IMDB_movies
) == 0:
3378 self
._displayMessage
(u
"1-No selection made for Movie(%s)" % title
)
3380 movies
= IMDB_movies
3383 inetref
= ui
.selectSeries(movies
)
3384 except tvdb_userabort
:
3385 self
._displayMessage
(u
"2-No selection made for Movie(%s)" % title
)
3388 if inetref
.has_key('sid'):
3389 if _can_int(inetref
['sid']):
3390 if inetref
['sid'] == '99999999':
3391 return inetref
['sid']
3393 if inetref
['name'] == u
'User input':
3395 data
= imdb_access
.get_movie(inetref
['sid'])
3396 if data
.has_key('long imdb title'):
3397 return {'name': data['long imdb title'], u'sid': inetref['sid']}
3398 elif data
.has_key('title'):
3399 return {'name': data['title'], u'sid': inetref['sid']}
3402 except imdb
._exceptions
.IMDbDataAccessError
:
3407 return u
"%07d" % int(inetref
['sid']) # Pad out IMDB# with leading zeroes
3414 def _getTmdbGraphics(self
, cfile
, graphic_type
, watched
=False):
3415 '''Download either a movie Poster or Fanart
3417 return full qualified path and filename of downloaded graphic
3419 if graphic_type
== u
'-P':
3420 graphic_name
= u
'poster'
3421 key_type
= u
'coverart'
3422 rel_type
= u
'coverfile'
3424 graphic_name
= u
'fanart'
3425 key_type
= u
'fanart'
3428 self
.config
['series_name']=cfile
['file_seriesname']
3430 if len(cfile
['inetref']) == 7: # IMDB number
3431 results
= self
.config
['tmdb_api'].searchIMDB(cfile
['inetref'])
3433 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3434 except TmdbMovieOrPersonNotFound
, e
:
3435 self
._displayMessage
(u
"0-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3437 except Exception, e
:
3438 self
._displayMessage
(u
"themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_name
, e
))
3442 if not results
.has_key(key_type
):
3443 self
._displayMessage
(u
"1-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3446 self
._displayMessage
(u
"1b-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3449 graphic_file
= (results
[key_type
].split(u
','))[0].strip() # Only want the first image URL
3451 self
.config
['g_defaultname']=False
3452 self
.config
['toprated'] = True
3453 self
.config
['nokeys'] = False
3455 self
.config
['sid']=None
3457 if self
.program_seriesid
== None:
3458 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season 1'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3460 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3462 self
.config
['g_series'] = cfile
['inetref']+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3463 if graphic_type
== '-P':
3468 self
.config
['season_num']= None # Needed to get graphics named in 'g_series' format
3470 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3472 tmp_URL
= graphic_file
.replace(u
"http://", u
"")
3473 graphic_file
= u
"http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
3474 value
= self
._downloadGraphics
(u
"%s:%s" % (g_type
, graphic_file
), mythtv
=True)
3476 self
.config
['overwrite'] = False # Turn off overwriting
3479 self
._displayMessage
(u
"2-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3482 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3483 # end _getTmdbGraphics
3485 def _getSecondarySourceGraphics(self
, cfile
, graphic_type
, watched
=False):
3486 '''Download from secondary source such as movieposter.com
3488 return full qualified path and filename of downloaded graphic
3490 if not len(self
.config
['myth_secondary_sources']):
3493 if graphic_type
== u
'coverfile':
3494 graphic_type
= u
'poster'
3495 rel_type
= u
'coverfile'
3497 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
3498 if not self
.config
['myth_secondary_sources'].has_key('movies'):
3500 if self
.config
['myth_secondary_sources']['movies'].has_key(graphic_type
):
3501 source
= self
.config
['myth_secondary_sources']['movies'][graphic_type
]
3502 if source
.find(u
'%(imdb)s') != -1:
3503 if len(cfile
['inetref']) != 7:
3505 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3506 except TmdbMovieOrPersonNotFound
, e
:
3507 self
._displayMessage
(u
"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_type
, e
))
3509 except Exception, e
:
3510 self
._displayMessage
(u
"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_type
, e
))
3514 if not results
.has_key('imdb'):
3515 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']))
3517 cfile
['imdb'] = results
['imdb']
3519 cfile
['imdb'] = cfile
['inetref']
3523 if not self
.config
['myth_secondary_sources'].has_key('tv'):
3525 if self
.config
['myth_secondary_sources']['tv'].has_key(graphic_type
):
3526 source
= self
.config
['myth_secondary_sources']['tv'][graphic_type
]
3530 self
.config
['series_name']=cfile
['file_seriesname']
3532 if self
.config
['simulation']:
3533 sys
.stdout
.write(u
"Simulating - downloading Secondary Source graphic (%s)\n" % cfile
['file_seriesname'])
3534 return u
"Simulated Secondary Source graphic filename place holder"
3536 # Test that the secondary's required data has been passed
3538 command
= source
% cfile
3540 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
))
3543 tmp_files
= callCommandLine(command
)
3545 self
._displayMessage
(u
"\n! Warning: Source (%s)\n could not find (%s) for (%s)(%s)\n" % (source
% cfile
, graphic_type
, cfile
['filename'], cfile
['inetref']))
3548 tmp_array
=tmp_files
.split('\n')
3549 if tmp_array
[0].startswith(u
'Failed'):
3550 self
._displayMessage
(u
"\n! Warning: Source (%s)\nfailed to download (%s) for (%s)(%s)\n" % (source
% cfile
, graphic_type
, cfile
['filename'], cfile
['inetref']))
3553 if tmp_array
[0].startswith(u
'file://'):
3554 tmp_files
=tmp_array
[0].replace(u
'file://', u
'')
3555 if not os
.path
.isfile(tmp_files
):
3556 sys
.stderr
.write(u
'\n! Error: The graphic file does not exist (%s)\n' % tmp_files
)
3559 # Fix file extentions in all caps or 4 character JPEG extentions
3560 fileExtension
= (_getExtention(tmp_files
)).lower()
3561 if fileExtension
== u
'jpeg':
3562 fileExtension
= u
'jpg'
3564 if self
.program_seriesid
== None:
3565 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], self
.sanitiseFileName(cfile
['file_seriesname']), self
.graphic_suffix
[rel_type
], fileExtension
)
3567 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], self
.sanitiseFileName(self
.program_seriesid
), self
.graphic_suffix
[rel_type
], fileExtension
)
3569 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], cfile
['inetref'], self
.graphic_suffix
[rel_type
], fileExtension
)
3571 if os
.path
.isfile(filename
): # This may be the same small file or worse then current
3573 (width
, height
) = self
.config
['image_library'].open(filename
).size
3574 (width2
, height2
) = self
.config
['image_library'].open(tmp_files
).size
3576 os
.remove(tmp_files
)
3581 # Verify that the downloaded file was NOT HTML instead of the intended file
3582 if self
._checkValidGraphicFile
(tmp_files
, graphicstype
=u
'', vidintid
=False) == False:
3583 os
.remove(tmp_files
) # Delete the useless HTML text
3585 shutil
.copy2(tmp_files
, filename
)
3586 os
.remove(tmp_files
)
3587 self
.num_secondary_source_graphics_downloaded
+=1
3588 return self
.rtnRelativePath(filename
, graphicsDirectories
[rel_type
])
3590 graphic_file
= tmp_array
[0]
3592 self
.config
['g_defaultname']=False
3593 self
.config
['toprated'] = True
3594 self
.config
['nokeys'] = False
3596 self
.config
['sid']=None
3598 if self
.program_seriesid
== None:
3599 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3601 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3603 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['inetref'])+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3604 g_type
= graphic_type
3606 self
.config
['season_num']= None # Needed to get graphics named in 'g_series' format
3608 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3610 tmp_URL
= graphic_file
.replace(u
"http://", u
"")
3611 graphic_file
= u
"http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
3612 value
= self
._downloadGraphics
(u
"%s:%s" % (g_type
, graphic_file
), mythtv
=True)
3614 self
.config
['overwrite'] = False # Turn off overwriting
3616 self
._displayMessage
(u
"Secondary source %s not found(%s)(%s)" % (graphic_file
, cfile
['filename'], cfile
['inetref']))
3619 self
.num_secondary_source_graphics_downloaded
+=1
3620 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3621 # end _getSecondarySourceGraphics
3623 def combineMetaData(self
, available_metadata
, meta_dict
, vid_type
=False):
3624 ''' Combine the current data with new meta data from primary or secondary sources
3625 return combinted meta data dictionary
3628 for key
in meta_dict
.keys():
3629 if key
in self
.config
['metadata_exclude_as_update_trigger']:
3632 if key
== 'inetref' and available_metadata
[key
] != meta_dict
[key
]:
3633 available_metadata
[key
] = meta_dict
[key
]
3635 if key
== 'releasedate' and available_metadata
[key
] != meta_dict
[key
]:
3636 available_metadata
[key
] = meta_dict
[key
]
3638 if key
== 'userrating' and available_metadata
[key
] == 0.0:
3639 available_metadata
[key
] = meta_dict
[key
]
3641 if key
== 'length' and available_metadata
[key
] == 0:
3642 available_metadata
[key
] = meta_dict
[key
]
3644 if key
== 'rating' and (available_metadata
[key
] == 'NR' or available_metadata
[key
] == 'Unknown'):
3645 available_metadata
[key
] = meta_dict
[key
]
3647 if key
== 'year' and available_metadata
[key
] == 1895:
3648 available_metadata
[key
] = meta_dict
[key
]
3650 if key
== 'category' and available_metadata
[key
] == 0:
3651 available_metadata
[key
] = meta_dict
[key
]
3653 if key
== 'inetref' and available_metadata
[key
] == '00000000':
3654 available_metadata
[key
] = meta_dict
[key
]
3657 available_metadata
[key
] = meta_dict
[key
]
3659 if vid_type
and key
== 'subtitle': # There are no subtitles in movies
3661 if key
== 'plot': # Remove any line-feeds from the plot. Mythvideo does not expect them.
3662 meta_dict
[key
] = meta_dict
[key
].replace('\n', ' ')
3663 if (vid_type
and key
== 'plot') and (meta_dict
[key
].find('@') != -1 or len(meta_dict
[key
].split(' ')) < 10):
3665 if vid_type
and key
== 'plot':
3666 if available_metadata
[key
] != None:
3667 if len(available_metadata
[key
].split(' ')) < 10 and len(meta_dict
[key
].split(' ')) > 10:
3668 available_metadata
[key
] = meta_dict
[key
]
3670 if not available_metadata
.has_key(key
): # Mainly for Genre, Cast and Countries
3671 available_metadata
[key
] = meta_dict
[key
]
3673 if available_metadata
[key
] == None or available_metadata
[key
] == '' or available_metadata
[key
] == 'None' or available_metadata
[key
] == 'Unknown':
3674 available_metadata
[key
] = meta_dict
[key
]
3676 return available_metadata
3677 # end combineMetaData
3680 def _getSecondarySourceMetadata(self
, cfile
, available_metadata
):
3681 '''Download meta data from secondary source
3682 return available_metadata (returns the current metadata unaltered)
3683 return dictionary of combined meta data
3685 if not len(self
.config
['myth_secondary_sources']):
3686 return available_metadata
3688 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
3689 if not self
.config
['myth_secondary_sources'].has_key('movies'):
3690 return available_metadata
3692 if self
.config
['myth_secondary_sources']['movies'].has_key('metadata'):
3693 source
= self
.config
['myth_secondary_sources']['movies']['metadata']
3694 if source
.find(u
'%(imdb)s') != -1:
3695 if len(cfile
['inetref']) != 7:
3697 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3698 except TmdbMovieOrPersonNotFound
, e
:
3699 self
._displayMessage
(u
"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile
['file_seriesname'], e
))
3700 return available_metadata
3701 except Exception, e
:
3702 self
._displayMessage
(u
"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile
['file_seriesname'], e
))
3703 return available_metadata
3705 return available_metadata
3706 if not results
.has_key('imdb'):
3707 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']))
3708 return available_metadata
3709 cfile
['imdb'] = results
['imdb']
3711 cfile
['imdb'] = cfile
['inetref']
3713 return available_metadata
3715 if not self
.config
['myth_secondary_sources'].has_key('tv'):
3716 return available_metadata
3718 if self
.config
['myth_secondary_sources']['tv'].has_key('metadata'):
3719 source
= self
.config
['myth_secondary_sources']['tv']['metadata']
3721 return available_metadata
3723 # Test that the secondary's required data has been passed
3725 command
= source
% cfile
3727 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
))
3728 return available_metadata
3730 self
.config
['series_name']=cfile
['file_seriesname']
3733 tmp_files
= (callCommandLine(command
)).decode("utf8")
3735 self
._displayMessage
(u
"1-Secondary source (%s)\ndid not find(%s)(%s) meta data dictionary cannot be returned" % (source
% cfile
, cfile
['filename'], cfile
['inetref']))
3736 return available_metadata
3739 tmp_array
=tmp_files
.split('\n')
3740 for element
in tmp_array
:
3741 element
= (element
.rstrip('\n')).strip()
3742 if element
== '' or element
== None:
3745 index
= element
.index(':')
3748 key
= element
[:index
].lower()
3749 data
= element
[index
+1:]
3750 if data
== None or data
== '':
3752 if key
== u
'inetref' and len(cfile
['inetref']) == 7:
3753 meta_dict
[key
] = cfile
['inetref']
3755 data
= self
._changeAmp
(data
)
3756 data
= self
._changeToCommas
(data
)
3759 meta_dict
[key
] = int(data
)
3763 if key
== 'userrating':
3765 meta_dict
[key
] = float(data
)
3769 if key
== 'runtime':
3771 meta_dict
['length'] = long(data
)
3775 if key
== 'movierating':
3776 meta_dict
['rating'] = data
3780 if len(data
.split(' ')) < 10: # Skip plots that are less than 10 words
3784 if key
== 'trailer':
3786 if key
== 'releasedate':
3788 meta_dict
[key
] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
3792 meta_dict
[key
] = data
3793 if not len(meta_dict
):
3794 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']))
3795 return available_metadata
3798 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=movie
)
3799 self
.num_secondary_source_metadata_downloaded
+=1
3800 return available_metadata
3801 # end _getSecondarySourceMetadata
3803 def _getTmdbMetadata(self
, cfile
, available_metadata
):
3804 '''Download a movie's meta data and massage the genres string
3805 return results for secondary sources when no primary source meta data
3806 return dictionary of metadata combined with data from a secondary source
3809 if len(cfile
['inetref']) == 7: # IMDB number
3810 meta_dict
= self
.config
['tmdb_api'].searchIMDB(cfile
['inetref'])
3812 meta_dict
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3813 except TmdbMovieOrPersonNotFound
, e
:
3814 self
._displayMessage
(u
"0-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3815 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3816 except Exception, e
:
3817 self
._displayMessage
(u
"themoviedb.com error for Movie(%s)(%s) meta data dictionary cannot be returned, error(%s)" % (cfile
['filename'], cfile
['inetref'], e
))
3818 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3820 if meta_dict
== None:
3821 self
._displayMessage
(u
"1-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3822 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3824 keys
= meta_dict
.keys()
3827 data
= meta_dict
[key
]
3830 if key
== 'homepage':
3832 data
= self
._changeAmp
(data
)
3833 data
= self
._changeToCommas
(data
)
3836 genre_array
= data
.split(',')
3837 for i
in range(len(genre_array
)):
3838 genre_array
[i
] = (genre_array
[i
].strip()).lower()
3839 if genre_array
[i
] in self
.config
['tmdb_genre_filter']:
3840 genres
+=genre_array
[i
].title()+','
3842 meta_dict
[key
] = u
''
3845 meta_dict
[key
] = genres
[:-1]
3846 if key
== 'trailer':
3850 meta_dict
[key
] = int(data
)
3854 if key
== 'userrating':
3856 meta_dict
[key
] = float(data
)
3861 meta_dict
['homepage'] = data
3863 if key
== 'releasedate':
3865 meta_dict
[key
] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
3869 if key
== 'runtime':
3871 meta_dict
['length'] = long(data
)
3875 if key
== 'movierating':
3876 meta_dict
['rating'] = data
3878 if meta_dict
.has_key('rating'):
3879 if meta_dict
['rating'] == '':
3880 meta_dict
['rating'] = 'Unknown'
3883 if available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None:
3884 filename
= u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
3885 meta_dict
['hash'] = self
.hashFile(filename
)
3886 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=True)
3887 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3889 self
._displayMessage
(u
"2-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3890 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3891 # end _getTmdbMetadata
3893 def _getTvdbGraphics(self
, cfile
, graphic_type
, toprated
=False, watched
=False):
3894 '''Download either a TV Series Poster, Banner, Fanart or Episode image
3896 return full qualified path and filename of downloaded graphic
3898 rel_type
= graphic_type
3899 if graphic_type
== u
'coverfile':
3900 graphic_type
= u
'poster'
3901 elif graphic_type
== u
'poster':
3902 rel_type
=u
'coverfile'
3904 self
.config
['g_defaultname']=False
3905 self
.config
['toprated'] = toprated
3906 self
.config
['nokeys'] = False
3907 self
.config
['maximum'] = u
'1'
3910 self
.config
['sid']=cfile
['inetref']
3912 self
.config
['sid']=None
3913 self
.config
['episode_name'] = None
3914 self
.config
['series_name']=cfile
['file_seriesname']
3916 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3917 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3919 # Special logic must be used if the (-MG) guessing option has been requested
3920 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
3922 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
3923 except Exception, e
:
3924 self
._displayMessage
(u
"tvdb Series not found(%s) or connection issues with thetvdb.com web site.\nError:(%s)\n" % (cfile
['filename'], e
))
3926 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
,cfile
['file_seriesname'].lower()):
3927 self
.config
['sid'] = allmatchingseries
['sid']
3928 self
.config
['series_name'] = allmatchingseries
['name']
3929 cfile
['file_seriesname'] = allmatchingseries
['name']
3931 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'])
3934 if not self
.verifySeriesExists():
3935 self
._displayMessage
(u
"tvdb Series not found(%s)" % cfile
['filename'])
3939 if self
.program_seriesid
== None:
3940 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season 1'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3941 self
.config
['g_season'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3943 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3944 self
.config
['g_season'] = self
.sanitiseFileName(self
.program_seriesid
)+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3946 # TV Series ALWAYS need the ' Season' in the file name incase the show name could clobber a Movie image
3947 # Season X is used so that a real season image is not overritten. It will be renamed later.
3948 self
.config
['g_series'] = self
.sanitiseFileName(self
.config
['series_name'])+u
' Season X'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3949 self
.config
['g_season'] = self
.sanitiseFileName(self
.config
['series_name'])+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3951 typegetGraphics
=self
.getTopRatedGraphics
3952 self
.config
['season_num']= None # Needed to get toprated graphics named in 'g_series' format
3954 typegetGraphics
=self
.getGraphics
3956 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3957 value
= self
._downloadGraphics
(typegetGraphics(graphic_type
), mythtv
=True)
3958 self
.config
['overwrite'] = False # Turn off overwriting
3962 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3963 # end _getTvdbGraphics
3965 def _getTvdbMetadata(self
, cfile
, available_metadata
):
3966 '''Download thetvdb.com meta data
3967 return what was input or results from a secondary source
3968 return dictionary of metadata
3970 global video_type
, UI_title
3971 video_type
=u
'TV series'
3972 UI_title
= cfile
['file_seriesname']
3975 self
.config
['nokeys'] = False
3976 self
.config
['sid']=None
3977 self
.config
['episode_name'] = None
3978 self
.config
['series_name']=cfile
['file_seriesname']
3979 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3980 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3981 if self
.config
['series_name_override']:
3982 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
3983 self
.config
['sid'] = (self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]).strip()
3985 # Special logic must be used if the (-MG) guessing option has been requested
3986 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
3988 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
3989 except Exception, e
:
3990 self
._displayMessage
(u
"tvdb Series not found(%s) or there are connection problems with thetvdb.com\nError(%s)" % (cfile
['filename'], e
))
3992 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
,cfile
['file_seriesname'].lower()):
3993 self
.config
['sid'] = allmatchingseries
['sid']
3994 self
.config
['series_name'] = allmatchingseries
['name']
3995 cfile
['file_seriesname'] = allmatchingseries
['name']
3997 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'])
4000 if not self
.verifySeriesExists():
4001 self
._displayMessage
(u
"tvdb Series not found(%s) meta data dictionary cannot be returned" % cfile
['filename'])
4002 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4004 if self
.config
['sid'] == '99999999':
4005 if not self
.config
['interactive']:
4006 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4008 return {'sid': self.config['sid'], 'title': cfile['file_seriesname']}
4011 tmp_array
=(self
.getSeriesEpisodeData()).split('\n')
4013 for element
in tmp_array
:
4014 element
= (element
.rstrip('\n')).strip()
4017 index
= element
.index(':')
4018 key
= element
[:index
].lower()
4019 data
= element
[index
+1:]
4023 meta_dict
['title'] = data
4025 if key
== 'seasonnumber':
4027 meta_dict
['season'] = int(data
)
4031 if key
== 'episodenumber':
4033 meta_dict
['episode'] = int(data
)
4037 if key
== 'episodename':
4038 meta_dict
['subtitle'] = data
4040 if key
== u
'overview':
4041 meta_dict
['plot'] = data
4043 if key
== u
'director' and data
== 'None':
4044 meta_dict
['director'] = ''
4046 if key
== u
'firstaired' and len(data
) > 4:
4048 meta_dict
['year'] = int(data
[:4])
4051 meta_dict
['firstaired'] = data
4053 meta_dict
['releasedate'] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
4059 meta_dict
['year'] = int(data
)
4063 if key
== 'seriesid':
4064 meta_dict
['inetref'] = data
4065 meta_dict
[key
] = data
4069 meta_dict
['userrating'] = float(data
)
4073 if key
== 'filename':# This "episodeimage URL clashed with the video file name and ep image
4074 continue # is not used yet. So skip fixes the db video filename from being wiped.
4075 if key
== 'runtime':
4077 meta_dict
['length'] = long(data
)
4081 meta_dict
[key
] = data
4084 if not meta_dict
.has_key('director'):
4085 meta_dict
['director'] = u
''
4086 meta_dict
['rating'] = u
'TV Show'
4087 # URL to TVDB web site episode web page for this series
4088 for url_data
in [u
'seriesid', u
'seasonid', u
'id']:
4089 if not url_data
in meta_dict
.keys():
4092 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'])
4093 if available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None:
4094 filename
= u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4095 meta_dict
['hash'] = self
.hashFile(filename
)
4096 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=False)
4097 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4099 self
._displayMessage
(u
"tvdb Series found (%s) but no meta data for dictionary" % cfile
['filename'])
4100 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4101 # end _getTvdbMetadata
4103 def _make_db_ready(self
, text
):
4104 '''Prepare text for inclusion into a DB
4106 return data base ready text
4108 if not text
: return text
4110 text
= text
.replace(u
'\u2013', u
"-")
4111 text
= text
.replace(u
'\u2014', u
"-")
4112 text
= text
.replace(u
'\u2018', u
"'")
4113 text
= text
.replace(u
'\u2019', u
"'")
4114 text
= text
.replace(u
'\u2026', u
"...")
4115 text
= text
.replace(u
'\u201c', u
'"')
4116 text
= text
.replace(u
'\u201d', u
'"')
4117 except UnicodeDecodeError:
4123 def _addCastGenreCountry(self
, data_string
, vim
, cast_genres_type
):
4124 '''From a comma delimited string of cast members, genres or countries add the ones
4125 not already in the myth db and update the video's meta data
4126 return True when successfull
4127 return False if failed
4129 if data_string
== '':
4131 data
= data_string
.split(',')
4132 for i
in range(len(data
)):
4133 data
[i
]=data
[i
].strip()
4139 if cast_genres_type
== 'genres':
4142 elif cast_genres_type
== 'cast':
4145 elif cast_genres_type
== 'countries':
4147 vim
.country
.add(item
)
4150 # end _addCastGenreCountry()
4156 def _moveDirectoryTree(self
, src
, dst
, symlinks
=False, ignore
=None):
4157 '''Move a directory tree from a given source to a given destination. Subdirectories will be
4158 created and synbolic links will be recreated in the new destination.
4159 return an array of two arrays. Names of files/directories moved and Errors found
4165 (src
, fileName
) = os
.path
.split(src
)
4167 names
= os
.listdir(unicode(src
, 'utf8'))
4168 except (UnicodeEncodeError, TypeError):
4169 names
= os
.listdir(src
)
4171 if os
.path
.isfile(src
):
4172 (src
, fileName
) = os
.path
.split(src
)
4176 names
= os
.listdir(unicode(src
, 'utf8'))
4177 except (UnicodeEncodeError, TypeError):
4178 names
= os
.listdir(src
)
4180 if ignore
is not None:
4181 ignored_names
= ignore(src
, names
)
4183 ignored_names
= set()
4186 if self
.config
['simulation']:
4187 sys
.stdout
.write(u
"Simulation creating subdirectories for file move (%s)\n" % dst
)
4189 self
._displayMessage
(u
"Creating subdirectories for file move (%s)\n" % dst
)
4190 os
.makedirs(dst
) # Some of the subdirectories may already exist
4195 if name
in ignored_names
:
4197 srcname
= os
.path
.join(src
, name
)
4198 dstname
= os
.path
.join(dst
, name
)
4200 if not os
.access(srcname
, os
.F_OK | os
.R_OK | os
.W_OK
): # Skip any file that is not RW able
4201 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
))
4204 if symlinks
and os
.path
.islink(srcname
):
4205 linkto
= os
.readlink(srcname
)
4206 if self
.config
['simulation']:
4207 sys
.stdout
.write(u
"Simulation recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto
, dstname
))
4209 os
.symlink(linkto
, dstname
)
4210 self
._displayMessage
(u
"Recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto
, dstname
))
4211 self
.num_symbolic_links
+=1
4212 elif os
.path
.isdir(srcname
):
4214 self
._displayMessage
(u
"Wildcard skipping subdirectory (%s)\n" % srcname
)
4216 self
.num_created_video_subdirectories
+=1
4217 self
._displayMessage
(u
"Move subdirectory (%s)\n" % srcname
)
4218 self
._moveDirectoryTree
(srcname
, dstname
, symlinks
, ignore
)
4220 if self
.config
['simulation']:
4222 if srcname
.startswith(org_src
[:-1]):
4223 sys
.stdout
.write(u
"Simulation move wild card file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4224 self
.num_moved_video_files
+=1
4225 self
.new_names
.append(dstname
)
4227 self
._displayMessage
(u
"Simulation of wildcard skipping file(%s)" % (srcname
,))
4229 sys
.stdout
.write(u
"Simulation move file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4230 self
.num_moved_video_files
+=1
4231 self
.new_names
.append(dstname
)
4234 if srcname
.startswith(org_src
[:-1]):
4235 self
._displayMessage
(u
"Move wild card file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4236 shutil
.move(srcname
, dstname
)
4237 self
.num_moved_video_files
+=1
4238 self
.new_names
.append(dstname
)
4240 self
._displayMessage
(u
"Wildcard skipping file(%s)" % (srcname
,))
4242 self
._displayMessage
(u
"Move file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4243 shutil
.move(srcname
, dstname
)
4244 self
.num_moved_video_files
+=1
4245 self
.new_names
.append(dstname
)
4246 # XXX What about devices, sockets etc.?
4247 except (IOError, os
.error
), why
:
4248 self
.errors
.append([srcname
, dstname
, str(why
)])
4249 # catch the Error from the recursive move tree so that we can
4250 # continue with other files
4252 self
.errors
.append([src
, dst
, u
"Unknown error"])
4254 return [self
.new_names
, self
.errors
]
4255 # end _moveDirectoryTree
4257 # local variable for move stats
4258 num_moved_video_files
=0
4259 num_created_video_subdirectories
=0
4260 num_symbolic_links
=0
4262 def _moveVideoFiles(self
, target_destination_array
):
4263 """Copy files or directories to a destination directory.
4264 If the -F filename option is set then rename TV series during the move process. The move will
4265 be interactive for identifying a movie's IMDB number or TV series if the -i option was also set.
4266 If there is a problem error message are displayed and the script exists. After processing
4267 print a statistics report.
4268 return a array of video file dictionaries to update in Mythvideo data base
4270 global UI_selectedtitle
4271 # Validate that the targets and destinations actually exist.
4273 for file_dir
in target_destination_array
:
4274 if os
.access(file_dir
, os
.F_OK | os
.R_OK
):
4276 # Destinations must all be directories
4277 if not os
.path
.isdir(file_dir
):
4278 sys
.stderr
.write(u
"\n! Error: Destinations must all be directories.\nThis destination is not a directory (%s)\n" % (file_dir
,))
4282 for directory
in self
.config
['mythvideo']:
4283 dummy_dir
= file_dir
.replace(directory
, u
'')
4284 if dummy_dir
!= tmp_dir
:
4287 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'], ))
4289 # Verify that a target file is really a video file.
4290 if file_dir
[-1:] != '*': # Skip wildcard file name targets
4291 if os
.access(file_dir
, os
.F_OK | os
.R_OK
): # Confirm that the file actually exists
4292 if not os
.path
.isdir(file_dir
):
4293 ext
= _getExtention(file_dir
)
4294 for tmp_ext
in self
.config
['video_file_exts']:
4295 if ext
.lower() == tmp_ext
:
4298 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'],))
4303 num_renamed_files
= 0
4304 num_mythdb_updates
= 0
4307 video_files_to_process
=[]
4309 while i
< len(target_destination_array
):
4310 src
= target_destination_array
[i
]
4312 if src
[-1:] == u
'*':
4315 (src
, fileName
) = os
.path
.split(src
)
4316 dst
= target_destination_array
[i
+1]
4320 results
= self
._moveDirectoryTree
(org_src
, dst
, symlinks
=False, ignore
=None)
4322 results
= self
._moveDirectoryTree
(src
, dst
, symlinks
=False, ignore
=None)
4323 if len(results
[1]): # Check if there are any errors
4324 sys
.stderr
.write(u
"\n! Warning: There were errors during moving, with these directories/files\n")
4325 for error
in results
[1]:
4326 sys
.stderr
.write(u
'\n! Warning: Source(%s), Destination(%s), Reason:(%s)\n' % (error
[0], error
[1], error
[2]))
4328 for name
in results
[0]:
4329 file_name
= os
.path
.join(dst
, name
)
4330 if os
.path
.isdir(file_name
):
4331 for dictionary
in self
._processNames
(_getFileList([file_name
]), verbose
= self
.config
['debug_enabled'], movies
=True):
4332 tmp_cfile_array
.append(dictionary
)
4334 for dictionary
in self
._processNames
([file_name
], verbose
= self
.config
['debug_enabled'], movies
=True):
4335 tmp_cfile_array
.append(dictionary
)
4337 # Is the source directory within a mythvideo directory? If it is,
4338 # update existing mythdb records else add the record as you already have the inetref
4339 for directory
in self
.config
['mythvideo']:
4340 if src
.startswith(directory
):
4341 for cfile
in tmp_cfile_array
:
4342 tmp_path
= src
+cfile
['filepath'].replace(dst
, u
'')
4343 video_file
= self
.rtnRelativePath(self
.movie_file_format
% (tmp_path
, cfile
['filename'], cfile
['ext']), 'mythvideo')
4344 tmp_filename
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), 'mythvideo')
4345 result
= mythvideo
.getVideo(exactfile
=video_file
)
4349 intid
= result
.intid
4351 result
= mythvideo
.getVideo(exactfile
=self
.movie_file_format
% (tmp_path
, cfile
['filename'], cfile
['ext']), host
=localhostname
.lower())
4355 intid
= result
.intid
4357 metadata
= Video(intid
, db
=mythvideo
)
4358 if tmp_filename
[0] == '/':
4360 self
.absolutepath
= True
4362 host
= localhostname
.lower()
4363 self
.absolutepath
= False
4365 if self
.config
['simulation']:
4366 sys
.stdout
.write(u
"Simulation Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file
, tmp_filename
))
4368 self
._displayMessage
(u
"Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file
, tmp_filename
))
4369 Video(intid
, db
=mythvideo
).update({'filename': tmp_filename, 'host': host}
)
4370 num_mythdb_updates
+=1
4374 cfile_array
.extend(tmp_cfile_array
)
4375 i
+=2 # Increment by 2 because array is int pairs of target and destination
4377 # Attempt to rename the video file
4378 if self
.config
['ret_filename']:
4379 for index
in range(len(cfile_array
)):
4380 cfile
= cfile_array
[index
]
4381 if self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']:
4382 sys
.stdout
.write(u
"\nAttempting to rename video filename (%s)\n" % cfile
['file_seriesname'])
4383 if cfile
['seasno'] == 0 and cfile
['epno'] == 0: # File rename for a movie
4386 if self
.config
['series_name_override']:
4387 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
4388 sid
= self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]
4390 data
= self
._getTmdbIMDB
(cfile
['file_seriesname'], rtnyear
=True)
4393 if data
[u
'sid'] == '99999999': # The user chose to ignore this video
4395 new_filename
= self
.sanitiseFileName(data
[u
'name'])
4399 imdb_access
= imdb
.IMDb()
4401 data
= imdb_access
.get_movie(sid
)
4402 if data
.has_key('long imdb title'):
4403 new_filename
= data
['long imdb title']
4404 elif data
.has_key('title'):
4405 new_filename
= self
.sanitiseFileName(namedata
['title'])
4408 except imdb
._exceptions
.IMDbDataAccessError
:
4411 if not sid
: # Cannot find this movie skip the renaming
4414 if not new_filename
:
4417 cfile_array
[index
]['file_seriesname'] = new_filename
4418 else: # File rename for a TV Series Episode
4419 UI_selectedtitle
= u
''
4421 self
.config
['sid'] = None
4422 self
.config
['series_name'] = cfile
['file_seriesname']
4423 if self
.config
['series_name_override']:
4424 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
4425 self
.config
['sid'] = self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]
4426 self
.config
['series_name'] = None
4427 self
.config
['season_num'] = u
"%d" % cfile
['seasno']
4428 self
.config
['episode_num'] = u
"%d" % cfile
['epno']
4429 self
.config
['episode_name'] = None
4430 new_filename
= self
.returnFilename()
4431 inetref
= self
.config
['sid']
4432 if inetref
== '99999999': # User chose to ignore this video
4436 if new_filename
== cfile
['filename']: # The file was already named to standard format
4437 self
._displayMessage
(u
"File is already the correct name(%s)\n" % cfile
['filename'])
4439 video_file
= self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4440 tmp_filename
= self
.movie_file_format
% (cfile
['filepath'], new_filename
, cfile
['ext'])
4441 if self
.config
['simulation']:
4442 sys
.stdout
.write(u
"Simulation file renamed from(%s) to(%s)\n" % (video_file
, tmp_filename
))
4444 if not os
.access(video_file
, os
.F_OK | os
.R_OK | os
.W_OK
):
4445 sys
.stdout
.write(u
"Cannot rename this file as it does not have read/write permissions set (%s)\n" % video_file
)
4447 self
._displayMessage
(u
"File renamed from(%s) to(%s)\n" % (video_file
, tmp_filename
))
4448 os
.rename(video_file
, tmp_filename
)
4449 num_renamed_files
+=1
4450 video_file
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), 'mythvideo')
4451 tmp_filename
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], new_filename
, cfile
['ext']), 'mythvideo')
4452 result
= mythvideo
.getVideo(exactfile
=video_file
)
4456 intid
= result
.intid
4458 result
= mythvideo
.getVideo(exactfile
=self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), host
=localhostname
.lower())
4462 intid
= result
.intid
4463 if tmp_filename
[0] == '/':
4465 self
.absolutepath
= True
4467 host
= localhostname
.lower()
4468 self
.absolutepath
= False
4470 metadata
= Video(intid
, db
=mythvideo
)
4471 if self
.config
['simulation']:
4472 sys
.stdout
.write(u
"Simulation Mythdb update for renamed file(%s)\n" % (tmp_filename
))
4474 self
._displayMessage
(u
"Mythdb update for renamed file(%s)\n" % (tmp_filename
))
4475 Video(intid
, db
=mythvideo
).update({'filename': tmp_filename, 'host': host}
)
4477 if self
.config
['simulation']:
4478 sys
.stdout
.write(u
"Simulation Mythdb add for renamed file(%s)\n" % (tmp_filename
))
4480 self
._displayMessage
(u
"Adding Mythdb record for file(%s)\n" % (tmp_filename
))
4482 initrec
[u
'title'] = cfile
['file_seriesname']
4483 initrec
[u
'filename'] = tmp_filename
4484 initrec
[u
'host'] = host
4485 initrec
[u
'inetref'] = inetref
4486 Video(db
=mythvideo
).create(initrec
)
4487 cfile_array
[index
]['filename'] = new_filename
4489 if self
.config
['simulation']:
4490 sys
.stdout
.write(u
'\n---------Simulated Statistics---------------')
4491 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
))
4494 # end _moveVideoFiles
4496 def _displayMessage(self
, message
):
4497 """Displays messages through stdout. Usually used with MythTv metadata updates in -V
4501 if message
[-1:] != '\n':
4503 if self
.config
['mythtv_verbose']:
4504 sys
.stdout
.write(message
)
4505 # end _displayMessage
4507 def _findMissingInetref(self
):
4508 '''Find any video file without a Mythdb record or without an inetref number
4509 return None if there are no new video files
4510 return a array of dictionary information on each video file that qualifies for processing
4512 directories
=self
.config
['mythvideo']
4514 if not len(directories
):
4515 sys
.stderr
.write(u
"\n! Error: There must be a video directory specified in MythTv\n")
4518 allFiles
= self
._findFiles
(directories
, self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
4519 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'], movies
=True)
4520 if len(validFiles
) == 0: # Is there video files to process?
4524 for cfile
in validFiles
:
4526 videopath
= self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4527 except UnicodeDecodeError:
4528 videopath
= os
.path
.join(unicode(cfile
['filepath'],'utf8'), unicode(cfile
['filename'],'utf8')+u
'.'+cfile
['ext'])
4530 # Find the MythTV meta data
4531 result
= mythvideo
.getVideo(exactfile
=videopath
)
4535 intid
= result
.intid
4537 result
= mythvideo
.getVideo(exactfile
=self
.rtnRelativePath(videopath
, 'mythvideo'), host
=localhostname
.lower())
4541 intid
= result
.intid
4543 missing_list
.append(cfile
)
4545 meta_dict
= Video(intid
, db
=mythvideo
)
4546 if self
.config
['video_dir']:
4547 if not mythvideo
.getVideo(exactfile
=meta_dict
[u
'filename'], host
=meta_dict
[u
'host']):
4548 missing_list
.append(cfile
)
4550 # There must be an Internet reference number. Get one for new records.
4551 if _can_int(meta_dict
['inetref']) and not meta_dict
['inetref'] == u
'00000000' and not meta_dict
['inetref'] == '':
4553 missing_list
.append(cfile
)
4556 # end _findMissingInetref
4558 def _checkValidGraphicFile(self
, filename
, graphicstype
=u
'', vidintid
=False):
4559 '''Verify that a graphics file is not really an HTML file
4560 return True if it is a graphics file
4561 return False if it is an HTML file
4563 # Verify that the graphics file is NOT HTML instead of the intended graphics file
4565 p
= subprocess
.Popen(u
'file "%s"' % filename
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
4567 # There is something wrong with the file but do NOT say it is invalid just in case!
4569 data
= p
.stdout
.readline()
4571 data
= data
.encode('utf8')
4572 except UnicodeDecodeError:
4573 data
= unicode(data
,'utf8')
4574 index
= data
.find(u
'HTML document text')
4577 elif self
.config
['simulation']:
4579 u
"Simulation deleting bad graphics file (%s) as it is really HTML\n" % (filename
, )
4583 u
"and the MythVideo record was corrected for the graphic reference.\n"
4587 os
.remove(filename
) # Delete the useless HTML text
4588 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
))
4591 if graphicstype
== u
'coverfile':
4592 repair
[graphicstype
] = u
'No Cover'
4594 repair
[graphicstype
] = u
''
4595 Video(vidintid
, db
=mythvideo
).update(repair
)
4597 # end _checkValidGraphicFile()
4600 def _graphicsCleanup(self
):
4601 '''Match the graphics in the mythtv graphics directories with the ones specified by the
4602 mythvideometa records. Remove any graphics that are not referenced at least once. Print a
4605 global localhostname
4609 stats
= {'coverfile': [0,0,0], 'banner': [0,0,0], 'fanart': [0,0,0]}
4611 graphics_file_dict
={}
4612 all_graphics_file_list
=[]
4613 for directory
in graphicsDirectories
.keys():
4614 if directory
== 'screenshot':
4616 file_list
= _getFileList(self
.config
[graphicsDirectories
[directory
]])
4617 if not len(file_list
):
4618 graphics_file_dict
[directory
] = []
4620 for g_file
in list(file_list
): # Cull the list removing dirs and non-graphics files
4621 if os
.path
.isdir(g_file
):
4622 file_list
.remove(g_file
)
4624 g_ext
= _getExtention(g_file
)
4625 if not g_ext
in self
.image_extensions
:
4626 file_list
.remove(g_file
)
4628 for filel
in file_list
:
4629 if not filel
in all_graphics_file_list
:
4630 all_graphics_file_list
.append(filel
)
4631 graphics_file_dict
[directory
] = file_list
4633 for key
in graphicsDirectories
.keys(): # Set initial totals
4634 if key
== 'screenshot':
4636 stats
[key
][num_total
] = len(graphics_file_dict
[key
])
4638 # Start reading videometadata records to remove their graphics from the image orphan list
4640 records
= mythvideo
.searchVideos()
4641 except MythError
, e
:
4642 sys
.stderr
.write(u
"\n! Error: Reading all videometadata records: %s\n" % e
.args
[0])
4645 atleast_one_video_file
= False
4647 for record
in records
:
4648 atleast_one_video_file
= True
4649 meta_dict
= {'host': record.host, 'coverfile': record.coverfile, 'banner': record.banner, 'fanart': record.fanart, 'filename': record.filename, 'intid': record.intid, 'inetref': record.inetref, }
4650 # Skip any videometadata record that is not for this host
4651 if meta_dict
['host'] != u
'' and meta_dict
['host'] != None:
4652 if meta_dict
['host'].lower() != localhostname
.lower():
4654 # Start removing any graphics in this videometadata record
4655 for key
in meta_dict
.keys():
4656 if key
in ['host','filename','intid', 'inetref']:
4658 if meta_dict
[key
] in [None, u
'', u
'None', u
'No Cover', u
'Unknown']:
4661 # Deal with videometadata record using storage groups
4662 if meta_dict
['filename'] != None:
4663 if meta_dict
['filename'][0] == u
'/':
4664 self
.absolutepath
= True
4666 self
.absolutepath
= False
4667 if meta_dict
[key
][0] != '/':
4668 meta_dict
[key
] = self
.rtnAbsolutePath(meta_dict
[key
], graphicsDirectories
[key
])
4669 if meta_dict
[key
][0] != '/': # There is not a storage group for this relative file name
4672 # Deal with TV series level graphics
4673 (dirName
, fileName
) = os
.path
.split(meta_dict
[key
])
4674 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
4675 index
= fileBaseName
.find(u
' Season ')
4676 intid
= meta_dict
['intid']
4678 if index
!= -1: # Is this a TV Series episode?
4679 if meta_dict
[key
] in graphics_file_dict
[key
]:
4680 if self
._checkValidGraphicFile
(meta_dict
[key
], graphicstype
=key
, vidintid
=intid
) == True:
4681 graphics_file_dict
[key
].remove(meta_dict
[key
])
4682 all_graphics_file_list
.remove(meta_dict
[key
])
4683 # This logic is specific to Movies and videos with a '99999999' inetref numbers
4684 elif fileName
.startswith(meta_dict
['inetref']+u
'_') or fileName
.startswith(meta_dict
['inetref']+u
'.') or meta_dict
['inetref'] == '99999999':
4685 if meta_dict
[key
] in graphics_file_dict
[key
]:
4686 if self
._checkValidGraphicFile
(meta_dict
[key
], graphicstype
=key
, vidintid
=intid
) == True:
4687 graphics_file_dict
[key
].remove(meta_dict
[key
])
4688 all_graphics_file_list
.remove(meta_dict
[key
])
4690 if not atleast_one_video_file
:
4691 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")
4693 # end reading videometadata records to remove their graphics from the image orphan list
4695 # Get Scheduled and Recorded program list
4696 programs
= self
._getScheduledRecordedProgramList
()
4698 # Remove Scheduled and Recorded program's graphics files from the delete list
4700 for field
in graphicsDirectories
.keys():
4701 if field
== 'screenshot':
4704 for graphic
in graphics_file_dict
[field
]:
4705 (dirName
, fileName
) = os
.path
.split(graphic
)
4706 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
4707 for program
in programs
:
4708 if fileBaseName
.lower().startswith(program
['title'].lower()+u
' '):
4709 remove
.append(graphic
)
4711 if not isValidPosixFilename(program
['title']) and program
['seriesid'] != u
'':
4712 if fileBaseName
.lower().startswith(program
['seriesid'].lower()):
4713 remove
.append(graphic
)
4716 if self
._checkValidGraphicFile
(rem
, graphicstype
=u
'', vidintid
=False) == True:
4717 graphics_file_dict
[field
].remove(rem
)
4719 all_graphics_file_list
.remove(rem
)
4720 except ValueError, e
:
4723 # Do not remove the MiroBridge default image files even if they are not currently being used
4724 for filel
in list(all_graphics_file_list
):
4725 if filel
.endswith('mirobridge_coverart.jpg'):
4726 all_graphics_file_list
.remove(filel
)
4728 if filel
.endswith('mirobridge_banner.jpg'):
4729 all_graphics_file_list
.remove(filel
)
4731 if filel
.endswith('mirobridge_fanart.jpg'):
4732 all_graphics_file_list
.remove(filel
)
4735 for key
in graphicsDirectories
.keys(): # Set deleted files totals
4736 if key
== 'screenshot':
4738 file_list
= list(graphics_file_dict
[key
])
4739 for filel
in file_list
:
4740 if not filel
in all_graphics_file_list
:
4741 graphics_file_dict
[key
].remove(filel
)
4742 stats
[key
][num_deleted
] = len(graphics_file_dict
[key
])
4744 # Delete all graphics files still on the delete list
4745 for filel
in all_graphics_file_list
:
4746 if self
.config
['simulation']:
4748 u
"Simulation deleting (%s)\n" % (filel
)
4755 self
._displayMessage
(u
"(%s) Has been deleted\n" % (filel
))
4757 for key
in graphicsDirectories
.keys(): # Set new files totals
4758 if key
== 'screenshot':
4760 stats
[key
][num_new_total
] = stats
[key
][num_total
] - stats
[key
][num_deleted
]
4762 if self
.config
['simulation']:
4763 sys
.stdout
.write(u
'\n\n------------Simulated Statistics---------------')
4764 sys
.stdout
.write(u
'\n--------------Janitor Statistics---------------\n')
4765 stat_type
= ['total', 'deleted', 'new total']
4766 for index
in range(len(stat_type
)):
4767 for key
in graphicsDirectories
.keys(): # Print stats
4768 if key
== 'screenshot':
4770 if key
== 'coverfile':
4774 sys
.stdout
.write(u
'% 9s % 7s ......................(% 5d)\n' % (stat_type
[index
], g_type
, stats
[key
][index
], ))
4776 for key
in graphicsDirectories
.keys(): # Print stats
4777 if key
== 'screenshot':
4779 if not len(graphics_file_dict
[key
]):
4781 if key
== 'coverfile':
4785 sys
.stdout
.write(u
'\n----------------Deleted %s files---------------\n' % g_type
)
4786 for graphic
in graphics_file_dict
[key
]:
4787 sys
.stdout
.write('%s\n' % graphic
)
4789 # end _graphicsCleanup
4791 def _getVideoLength(self
, videofilename
):
4792 '''Using ffmpeg (if it can be found) get the duration of the video
4793 return False if either ffmpeg cannot be found or the file is not a video
4794 return video lenght in minutes
4796 if not self
.config
['ffmpeg']:
4799 # Filter out specific file types due to potential negative processing overhead
4800 if _getExtention(videofilename
) in [u
'iso', u
'img', u
'VIDEO_TS', u
'm2ts', u
'vob']:
4803 p
= subprocess
.Popen(u
'ffmpeg -i "%s"' % (videofilename
), shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
4807 data
= p
.stderr
.readline()
4808 if data
.endswith('not found\n'):
4809 ffmpeg_found
= False
4811 if data
.startswith(' Duration:'):
4813 if data
== '' and p
.poll() != None:
4816 if ffmpeg_found
== False:
4817 self
.config
['ffmpeg'] = False
4820 time
= (data
[data
.index(':')+1: data
.index('.')]).strip()
4821 return (60*(int(time
[:2]))+(int(time
[3:5])))
4824 # end _getVideoLength
4827 def _getMiroVideometadataRecords(self
):
4828 """Fetches all videometadata records with an inetref of '99999999' and a category of 'Miro'. If the
4829 videometadata record has a host them it must match the lower-case of the locahostname.
4830 aborts if processing failed
4831 return and array of matching videometadata dictionary records
4833 global localhostname
4836 records
= mythvideo
.searchVideos(category
=u
'Miro', custom
=(('inetref=%s','99999999'),))
4837 except MythError
, e
:
4838 sys
.stderr
.write(u
"\n! Error: Reading all Miro videometadata records: %s\n" % e
.args
[0])
4841 for record
in records
:
4842 intids
.append(record
.intid
)
4844 videometadatarecords
=[]
4846 for intid
in intids
:
4847 vidrec
= Video(intid
, db
=mythvideo
)
4848 if vidrec
[u
'host'] != u
'' and vidrec
[u
'host'] != None:
4849 if vidrec
[u
'host'].lower() != localhostname
.lower():
4851 videometadatarecords
.append(vidrec
)
4853 return videometadatarecords
4856 # end _getMiroVideometadataRecords()
4858 def _getExtraMiroDetails(self
, mythvideorec
, vidtype
):
4859 '''Find the extra details required for Miro MythVideo record processing
4860 return a dictionary of details required for processing
4863 extradata
[u
'intid'] = [mythvideorec
[u
'intid']]
4864 if vidtype
== u
'movies':
4865 extradata
[u
'tv'] = False
4867 extradata
[u
'tv'] = True
4869 for key
in [u
'coverfile', u
'banner', u
'fanart', ]:
4870 extradata
[key
] = True # Set each graphics type as if it has already been downloaded
4871 if mythvideorec
[key
] == None or mythvideorec
[key
] == u
'No Cover' or mythvideorec
[key
] == u
'':
4872 extradata
[key
] = False
4874 elif key
== u
'coverfile': # Look for undersized coverart
4875 if mythvideorec
[u
'filename'][0] == u
'/':
4876 self
.absolutepath
= True
4878 self
.absolutepath
= False
4879 filename
= self
.rtnAbsolutePath(mythvideorec
[key
], graphicsDirectories
[key
])
4881 (width
, height
) = self
.config
['image_library'].open(filename
).size
4882 if width
< self
.config
['min_poster_size']:
4883 extradata
[key
] = False
4886 extradata
[key
] = False
4889 else: # Check if the default graphics are being used
4890 if mythvideorec
[key
].endswith(u
'mirobridge_banner.jpg'):
4891 extradata
[key
] = False
4892 if mythvideorec
[key
].endswith(u
'mirobridge_fanart.jpg'):
4893 extradata
[key
] = False
4896 if vidtype
== u
'movies': # Data specific to Movie Trailers
4897 if mythvideorec
[u
'filename'][0] == u
'/':
4898 self
.absolutepath
= True
4900 self
.absolutepath
= False
4901 extradata
[u
'filename'] = mythvideorec
[u
'filename']
4902 extradata
[u
'pathfilename'] = self
.rtnAbsolutePath(mythvideorec
[u
'filename'], u
'mythvideo')
4903 if os
.path
.islink(extradata
[u
'pathfilename']):
4904 extradata
[u
'symlink'] = True
4906 extradata
[u
'symlink'] = False
4907 moviename
= mythvideorec
['subtitle']
4911 index
= moviename
.find(self
.config
[u
'mb_movies'][filter(is_not_punct_char
, mythvideorec
[u
'title'].lower())])
4913 moviename
= moviename
[:index
].strip()
4914 extradata
[u
'moviename'] = moviename
4915 extradata
[u
'inetref'] = False
4916 if not moviename
== None and not moviename
== '':
4917 lastyear
= int(datetime
.datetime
.now().strftime(u
"%Y"))
4920 while i
< 5: # Check for a Movie that will be released this year or the next four years
4921 years
.append(u
"%d" % ((lastyear
+i
)))
4923 imdb_access
= imdb
.IMDb()
4926 movies_found
= imdb_access
.search_movie(moviename
.encode("ascii", 'ignore'))
4930 for movie
in movies_found
: # Get rid of duplicates
4931 if movie
.has_key('year'):
4932 temp
= {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])}
4933 if tmp_movies
.has_key(temp
.keys()[0]):
4935 tmp_movies
[temp
.keys()[0]] = temp
[temp
.keys()[0]]
4937 for movie
in tmp_movies
:
4938 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
:
4939 extradata
[u
'inetref'] = u
"%07d" % int(movie
)
4940 extradata
[u
'moviename'] = tmp_movies
[movie
]
4941 extradata
[u
'year'] = year
4943 if extradata
[u
'inetref']:
4946 # end _getExtraMiroDetails()
4948 def updateMiroVideo(self
, program
):
4949 '''Update the information in a Miro/MythVideo record
4952 global localhostname
, graphicsDirectories
4954 mirodetails
= program
[u
'miro']
4956 for intid
in mirodetails
[u
'intid']:
4958 for key
in graphicsDirectories
.keys():
4959 if key
== u
'screenshot':
4961 if mirodetails
[key
] != True and mirodetails
[key
] != False and mirodetails
[key
] != None and mirodetails
[key
] != u
'Simulated Secondary Source graphic filename place holder':
4962 # A graphics was downloaded
4963 changed_fields
[key
] = mirodetails
[key
]
4965 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
):
4966 changed_fields
[u
'inetref'] = mirodetails
[u
'inetref']
4967 changed_fields
[u
'subtitle'] = u
''
4968 changed_fields
[u
'year'] = mirodetails
[u
'year']
4969 changed_fields
[u
'banner'] = u
''
4970 (dirName
, fileName
) = os
.path
.split(mirodetails
[u
'pathfilename'])
4971 (fileBaseName
, fileExtension
) = os
.path
.splitext(fileName
)
4973 dir_list
= os
.listdir(unicode(dirName
, 'utf8'))
4974 except (UnicodeEncodeError, TypeError):
4975 dir_list
= os
.listdir(dirName
)
4978 filename
= self
.sanitiseFileName(u
'%s - Trailer %d' % (mirodetails
[u
'moviename'], index
))
4979 fullfilename
= u
'%s/%s%s' % (dirName
, filename
, fileExtension
)
4980 for flenme
in dir_list
:
4981 if fnmatch
.fnmatch(flenme
.lower(), u
'%s.*' % filename
.lower()):
4984 changed_fields
[u
'title'] = filename
4985 if self
.config
['simulation']:
4987 u
"Simulation rename Miro-MythTV movie trailer from (%s) to (%s)\n" % (mirodetails
[u
'pathfilename'], fullfilename
))
4989 os
.rename(mirodetails
[u
'pathfilename'], fullfilename
)
4990 changed_fields
[u
'filename'] = self
.rtnRelativePath(fullfilename
, u
'mythvideo')
4991 if changed_fields
[u
'filename'][0] != u
'/':
4992 changed_fields
[u
'host'] = localhostname
.lower()
4993 else: # Deal with the whole mixing Video SG and local with SG graphics mess
4994 for key
in graphicsDirectories
.keys():
4995 if key
== u
'screenshot' or not changed_fields
.has_key(key
):
4997 if changed_fields
[key
][0] == u
'/':
5000 changed_fields
.remove(key
)
5004 if len(changed_fields
):
5005 if self
.config
['simulation']:
5006 if program
['subtitle']:
5008 u
"Simulation MythTV DB update for Miro video (%s - %s)\n" % (program
['title'], program
['subtitle']))
5011 u
"Simulation MythTV DB update for Miro video (%s)\n" % (program
['title'],))
5013 Video(intid
, db
=mythvideo
).update(changed_fields
)
5014 # end updateMiroVideo()
5016 def _getScheduledRecordedProgramList(self
):
5017 '''Find all Scheduled and Recorded programs
5018 return array of found programs, if none then empty array is returned
5020 global localhostname
5023 # Get pending recordings
5025 progs
= mythbeconn
.getUpcomingRecordings()
5026 except MythError
, e
:
5027 sys
.stderr
.write(u
"\n! Error: Getting Upcoming Recordings list: %s\n" % e
.args
[0])
5032 if prog
.title
== None:
5034 record
['title'] = prog
.title
5035 record
['subtitle'] = prog
.subtitle
5036 record
['seriesid'] = prog
.seriesid
5038 if record
['subtitle'] and prog
.airdate
!= None:
5039 record
['originalairdate'] = prog
.airdate
.year
5041 if prog
.year
!= '0':
5042 record
['originalairdate'] = prog
.year
5043 elif prog
.airdate
!= None:
5044 record
['originalairdate'] = prog
.airdate
.year
5045 for program
in programs
: # Skip duplicates
5046 if program
['title'] == record
['title']:
5049 programs
.append(record
)
5051 # Get recorded records
5053 recordedlist
= list(mythdb
.searchRecorded(hostname
=localhostname
))
5054 except MythError
, e
:
5055 sys
.stderr
.write(u
"\n! Error: Getting recorded programs list: %s\n" % e
.args
[0])
5058 if not recordedlist
:
5061 recordedprogram
= {}
5062 for recordedRecord
in recordedlist
:
5063 if recordedRecord
.recgroup
== u
'Deleted':
5066 if recordedRecord
.title
== None:
5068 if recordedRecord
.chanid
== 9999:
5069 recorded
[u
'miro_tv'] = True
5070 recorded
[u
'title'] = recordedRecord
.title
5071 recorded
[u
'subtitle'] = recordedRecord
.subtitle
5072 recorded
[u
'seriesid'] = recordedRecord
.seriesid
5073 for program
in programs
: # Skip duplicates
5074 if program
['title'] == recorded
['title']:
5077 programs
.append(recorded
)
5078 # Get Release year for recorded movies
5079 # Get Recorded videos recordedprogram / airdate
5081 recordedDetails
= dict(RecordedProgram
.fromRecorded(recordedRecord
))
5082 except MythError
, e
:
5083 sys
.stderr
.write(u
"\n! Error: Getting recordedprogram table record: %s\n" % e
.args
[0])
5085 if not len(recordedDetails
):
5087 if not recordedDetails
['subtitle']:
5088 recordedprogram
[recordedDetails
['title']]= u
'%d' % recordedDetails
['airdate']
5090 # Add release year to recorded movies
5091 for program
in programs
:
5092 if recordedprogram
.has_key(program
['title']):
5093 program
['originalairdate'] = recordedprogram
[program
['title']]
5096 # Add real names to mb_tv if they are among the recorded videos
5097 if len(self
.config
['mb_tv_channels']):
5098 for program
in programs
:
5099 programtitle
= filter(is_not_punct_char
, program
[u
'title'].lower())
5100 if programtitle
in self
.config
['mb_tv_channels'].keys():
5101 self
.config
['mb_tv_channels'][programtitle
][1] = program
[u
'title']
5103 # Check that each program has an original airdate
5104 for program
in programs
:
5105 if not program
.has_key('originalairdate'):
5106 program
['originalairdate'] = u
'0000' # Set the original airdate to zero (unknown)
5108 # If there are any Miro TV or movies to process then add them to the list
5109 if len(self
.config
['mb_tv_channels']) or len(self
.config
['mb_movies']):
5110 miromythvideorecs
= self
._getMiroVideometadataRecords
()
5111 if miromythvideorecs
:
5112 # Create array used to check for duplicates
5115 for program
in programs
:
5116 programtitle
= filter(is_not_punct_char
, program
[u
'title'].lower())
5117 if programtitle
in self
.config
['mb_tv_channels'].keys():
5118 if not program
[u
'title'] in duplicatekeys
:
5119 duplicatekeys
[program
[u
'title']] = i
5120 elif programtitle
in self
.config
['mb_movies'].keys():
5121 moviename
= program
['subtitle']
5125 index
= moviename
.find(self
.config
['mb_movies'][programtitle
])
5127 moviename
= moviename
[:index
].strip()
5128 if not moviename
in duplicatekeys
:
5129 duplicatekeys
[moviename
] = i
5132 for record
in miromythvideorecs
:
5134 program
[u
'title'] = record
[u
'title']
5135 program
[u
'subtitle'] = record
[u
'subtitle']
5136 program
[u
'originalairdate'] = record
[u
'year']
5137 recordtitle
= filter(is_not_punct_char
, record
[u
'title'].lower())
5138 if recordtitle
in self
.config
['mb_tv_channels'].keys():
5139 if not record
[u
'title'] in duplicatekeys
.keys():
5140 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'tv')
5141 duplicatekeys
[program
[u
'title']] = len(programs
)
5142 programs
.append(program
)
5143 self
.config
['mb_tv_channels'][recordtitle
][1] = record
[u
'title']
5144 elif programs
[duplicatekeys
[program
[u
'title']]].has_key(u
'miro'):
5145 programs
[duplicatekeys
[program
[u
'title']]][u
'miro'][u
'intid'].append(record
[u
'intid'])
5147 programs
[duplicatekeys
[program
[u
'title']]][u
'miro'] = self
._getExtraMiroDetails
(record
, u
'tv')
5148 elif recordtitle
in self
.config
['mb_movies'].keys():
5149 moviename
= record
['subtitle']
5153 index
= moviename
.find(self
.config
['mb_movies'][filter(is_not_punct_char
, program
[u
'title'].lower())])
5155 moviename
= moviename
[:index
].strip()
5156 if not moviename
in duplicatekeys
.keys():
5157 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5158 if program
[u
'miro'][u
'inetref']:
5159 duplicatekeys
[moviename
] = len(programs
)
5160 programs
.append(program
)
5161 elif programs
[duplicatekeys
[moviename
]].has_key(u
'miro'):
5162 programs
[duplicatekeys
[moviename
]][u
'miro'][u
'intid'].append(record
[u
'intid'])
5164 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5165 if program
[u
'miro'][u
'inetref']:
5166 programs
[duplicatekeys
[moviename
]][u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5168 # Check that each program has seriesid
5169 for program
in programs
:
5170 if not program
.has_key('seriesid'):
5171 program
['seriesid'] = u
'' # Set an empty seriesid - Generall only for Miro Videos
5172 if program
['seriesid'] == None:
5173 program
['seriesid'] = u
'' # Set an empty seriesid
5176 # end _getScheduledRecordedProgramList
5179 def _getScheduledRecordedTVGraphics(self
, program
, graphics_type
):
5180 '''Get TV show graphics for Scheduled and Recorded TV programs
5181 return None if no graphics found
5182 return fullpath and filename of downloaded graphics file
5184 if graphics_type
== 'coverfile':
5185 graphics_type
= 'poster'
5187 self
.config
['sid'] = None
5188 if self
.config
['series_name_override']:
5189 if self
.config
['series_name_override'].has_key(program
['title'].lower()):
5190 self
.config
['sid'] = self
.config
['series_name_override'][program
['title'].lower()]
5191 # Find out if there are any Series level graphics available
5192 self
.config
['toprated'] = True
5193 self
.config
['episode_name'] = None
5194 self
.config
['series_name'] = program
['title']
5195 self
.config
['season_num'] = None
5196 self
.config
['episode_num'] = None
5198 series_graphics
= self
.getGraphics(graphics_type
)
5200 if series_graphics
!= None:
5201 cfile
= { 'file_seriesname': program
['title'],
5202 'inetref': self
.config
['sid'],
5203 'seasno': self
.config
['season_num'],
5204 'epno': self
.config
['episode_num'],
5206 'filename': program
['title'],
5210 return self
._getTvdbGraphics
(cfile
, graphics_type
, toprated
=True, watched
=True)
5212 # end _getScheduledRecordedTVGraphics
5214 def _downloadScheduledRecordedGraphics(self
):
5215 '''Get Scheduled and Recorded programs and Miro vidoes get their graphics if not already
5217 return (nothing is returned)
5219 global localhostname
5221 # Initialize reporting stats
5222 total_progs_checked
= 0
5223 total_posters_found
= 0
5224 total_banners_found
= 0
5225 total_fanart_found
= 0
5226 total_posters_downloaded
= 0
5227 total_banners_downloaded
= 0
5228 total_fanart_downloaded
= 0
5230 total_miro_movies
= 0
5232 programs
= self
._getScheduledRecordedProgramList
()
5234 if not len(programs
): # Is there any programs to process?
5237 # Add any Miro Bridge mb_tv dictionary items to 'series_name_override' dictionary
5238 if not self
.config
['series_name_override'] and len(self
.config
['mb_tv_channels']):
5239 self
.config
['series_name_override'] = {}
5240 for miro_tv_key
in self
.config
['mb_tv_channels'].keys():
5241 if self
.config
['mb_tv_channels'][miro_tv_key
][0]:
5242 self
.config
['series_name_override'][self
.config
['mb_tv_channels'][miro_tv_key
][1].lower()] = self
.config
['mb_tv_channels'][miro_tv_key
][0]
5244 total_progs_checked
= len(programs
)
5246 # Get totals of Miro TV shows and movies that will be processed
5247 for program
in programs
:
5248 if program
.has_key(u
'miro'):
5249 if not program
[u
'miro'][u
'tv']:
5250 total_miro_movies
+=1
5253 elif program
.has_key(u
'miro_tv'):
5254 if filter(is_not_punct_char
, program
[u
'title'].lower()) in self
.config
['mb_movies'].keys():
5255 total_miro_movies
+=1
5259 # Prossess all TV shows and Movies
5260 for program
in programs
:
5261 program
['need'] = False # Initalize that this program does not need graphic(s) downloaded
5263 program_override_tv
= False
5264 # Check if a subtitle-less program is really a TV show with an override. This compensates for
5265 # poor EPG data sources (as has been reported from at least Australia)
5266 if not program
['subtitle'] and program
['title'].lower() in self
.config
['series_name_override']:
5268 result
= self
._searchforSeries
(program
['title'])
5269 program_override_tv
= True
5270 except Exception, e
:
5273 # Even movies get the ' Season' added to the image names so that movie such as '1408' do not clash
5274 # with TMDB#ed image names
5275 pattern
= u
'%s Season*.*'
5276 if not program
.has_key(u
'miro'):
5277 if program
['subtitle'] or program_override_tv
:
5278 graphics_name
= program
['title']
5280 if not int(program
['originalairdate']):
5281 graphics_name
= program
['title']
5283 graphics_name
= "%s (%s)" % (program
['title'], program
['originalairdate'])
5285 mirodetails
= program
[u
'miro']
5286 if mirodetails
[u
'tv']:
5287 graphics_name
= program
['title']
5289 graphics_name
= mirodetails
[u
'inetref']
5291 self
.absolutepath
= False # All Scheduled Recorded and Miro videos start in the SG "Default"
5293 # Search for graphics that are already downloaded
5294 for directory
in graphicsDirectories
.keys():
5295 if directory
== 'screenshot': # There is no downloading of screenshots required
5296 program
[directory
] = True
5298 if directory
== 'banner' and not program
['subtitle']: # No banners for movies
5299 program
[directory
] = True
5302 if not mirodetails
[u
'tv'] and directory
== 'banner': # No banners for movies
5303 program
[directory
] = True
5306 filename
= program
['title']
5307 elif mirodetails
[u
'tv']:
5308 filename
= program
['title']
5310 filename
= mirodetails
[u
'inetref']
5312 # Deal with TV series names that would generate invalid file names for images TV and movies
5313 self
.program_seriesid
= None
5314 if not isValidPosixFilename(filename
) and program
['seriesid'] != u
'':
5315 filename
= program
['seriesid']
5316 self
.program_seriesid
= program
['seriesid']
5318 # Actual check for existing graphics
5319 for dirct
in self
.config
[graphicsDirectories
[directory
]]:
5321 dir_list
= os
.listdir(unicode(dirct
, 'utf8'))
5322 except (UnicodeEncodeError, TypeError):
5323 dir_list
= os
.listdir(dirct
)
5324 for flenme
in dir_list
:
5325 if fnmatch
.fnmatch(flenme
.lower(), (pattern
% filename
).lower()):
5326 program
[directory
] = True
5327 if directory
== 'coverfile':
5328 total_posters_found
+=1
5329 elif directory
== 'banner':
5330 total_banners_found
+=1
5332 total_fanart_found
+=1
5333 if mirodetails
: # Update the Miro MythVideo records with any existing graphics
5334 mirodetails
[directory
] = self
.rtnRelativePath(u
'%s/%s' % (dirct
, flenme
), directory
)
5340 program
['need'] = True
5341 program
[directory
] = False
5343 # Check if there are any graphics to download
5344 if not program
['need']:
5346 filename
= program
['title']
5347 elif mirodetails
[u
'tv']:
5348 filename
= program
['title']
5350 filename
= mirodetails
[u
'moviename']
5351 self
._displayMessage
("All Graphics already downloaded for [%s]" % filename
)
5352 if mirodetails
: # Update the Miro MythVideo records with any new graphics
5353 self
.updateMiroVideo(program
)
5357 # It is more efficient to find inetref of movie once
5358 if not program
['subtitle'] and not program_override_tv
:
5359 if not program
.has_key('inetref'): # Was the inetref number already found?
5360 inetref
= self
._getTmdbIMDB
(graphics_name
, watched
=True)
5362 self
._displayMessage
("No movie inetref [%s]" % graphics_name
)
5363 # Fake subtitle as this may be a TV series without a subtitle
5364 program
['subtitle']=' '
5366 self
._displayMessage
("Found movie inetref (%s),[%s]" % (inetref
, graphics_name
))
5367 program
['inetref'] = inetref
5368 elif not mirodetails
[u
'tv']:
5369 program
['inetref'] = mirodetails
[u
'inetref']
5371 # Download missing graphics
5372 for key
in graphicsDirectories
.keys():
5373 if program
[key
]: # Check if this type of graphic is already downloaded
5375 miromovieflag
= False
5377 if not mirodetails
[u
'tv']:
5378 miromovieflag
= True
5379 # This is a TV episode or Miro TV show
5380 if (program
['subtitle'] or program_override_tv
) and not miromovieflag
:
5381 results
= self
._getScheduledRecordedTVGraphics
(program
, key
)
5384 filename
= program
['title']
5385 elif mirodetails
[u
'tv']:
5386 filename
= program
['title']
5388 filename
= mirodetails
[u
'moviename']
5389 if key
== 'coverfile':
5390 total_posters_downloaded
+=1
5391 elif key
== 'banner':
5392 total_banners_downloaded
+=1
5393 elif key
== 'fanart':
5394 total_fanart_downloaded
+=1
5395 if mirodetails
: # Save the filename for storing later
5396 mirodetails
[key
] = results
5398 self
._displayMessage
("TV Series - No (%s) for [%s]" % (key
, program
['title']))
5399 else: # This is a movie
5400 title
= program
['title']
5401 filename
= program
['title']
5403 title
= mirodetails
[u
'inetref']
5404 filename
= mirodetails
[u
'inetref']
5405 cfile
= { 'file_seriesname': title
,
5406 'inetref': program
['inetref'],
5410 'filename': filename
,
5414 if key
== 'coverfile':
5418 results
= self
._getTmdbGraphics
(cfile
, g_type
, watched
=True)
5420 results
= self
._getSecondarySourceGraphics
(cfile
, key
, watched
=True)
5422 if key
== 'coverfile':
5423 total_posters_downloaded
+=1
5424 elif key
== 'fanart':
5425 total_fanart_downloaded
+=1
5426 if mirodetails
: # Save the filename for storing later
5427 mirodetails
[key
] = results
5430 filename
= program
['title']
5432 filename
= mirodetails
[u
'moviename']
5433 self
._displayMessage
("No (%s) for [%s]" % (key
, filename
))
5435 if mirodetails
: # Update the Miro MythVideo records with any new graphics
5436 self
.updateMiroVideo(program
)
5439 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
))
5442 sys
.stdout
.write(u
'\n-------------Scheduled & Recorded----------\n')
5443 for program
in programs
:
5444 if not program
.has_key(u
'miro'):
5445 if program
.has_key(u
'miro_tv'):
5446 if filter(is_not_punct_char
, program
[u
'title'].lower()) in self
.config
['mb_movies'].keys():
5447 sys
.stdout
.write(u
'Miro Movie Trailer: %s\n' % (program
['title'], ))
5449 sys
.stdout
.write(u
'Miro TV Show: %s\n' % (program
['title'], ))
5451 if program
['subtitle']:
5452 sys
.stdout
.write(u
'%s\n' % (program
['title'], ))
5454 if program
['originalairdate'] != u
'0000':
5455 sys
.stdout
.write(u
'%s\n' % ("%s (%s)" % (program
['title'], program
['originalairdate'])))
5457 sys
.stdout
.write(u
'%s\n' % (program
['title'], ))
5458 elif program
[u
'miro'][u
'tv']:
5459 sys
.stdout
.write(u
'Miro TV Show: %s\n' % (program
['title'], ))
5461 sys
.stdout
.write(u
'Miro Movie Trailer: %s\n' % (program
[u
'miro'][u
'moviename'], ))
5463 # end _downloadScheduledRecordedGraphics()
5466 def findFileInDir(self
, filename
, directories
, suffix
=None, fuzzy_match
=False):
5467 '''Find if a file is in any of the specified directories. An exact match or a variation.
5468 return False - File not found in directories
5469 return True - Absolute file name and path
5471 (dirName
, fileName
) = os
.path
.split(filename
)
5472 (fileBaseName
, fileExtension
) = os
.path
.splitext(fileName
)
5473 if fuzzy_match
: # Match even when the names are not exactly the same by removing punctuation
5474 for dirct
in directories
:
5476 dir_list
= os
.listdir(unicode(dirct
, 'utf8'))
5477 except (UnicodeEncodeError, TypeError):
5478 dir_list
= os
.listdir(dirct
)
5480 for file_name
in dir_list
:
5481 match_list
.append(filter(is_not_punct_char
, file_name
.lower()))
5483 if fileBaseName
.find(suffix
) == -1:
5484 file_path
= filter(is_not_punct_char
, (u
"%s%s%s" % (fileBaseName
, suffix
, fileExtension
)).lower())
5485 file_path2
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5487 file_path
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5488 file_path2
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
.replace(suffix
, u
''), fileExtension
)).lower())
5489 if file_path
in match_list
:
5490 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path
)])
5491 if file_path2
in match_list
:
5492 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path2
)])
5495 file_path
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5496 if file_path
in match_list
:
5497 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path
)])
5500 else: # Find an exact match
5501 for directory
in directories
:
5502 if filename
[0] != u
'/' and dirName
!= u
'':
5503 dir_name
= u
"%s/%s" % (directory
, dirName
)
5505 dir_name
= directory
5507 if fileBaseName
.find(suffix
) == -1:
5508 file_path
= u
"%s/%s%s%s" % (dir_name
, fileBaseName
, suffix
, fileExtension
)
5509 file_path2
= u
'%s/%s' % (dir_name
, fileName
)
5511 file_path
= u
'%s/%s' % (dir_name
, fileName
)
5512 file_path2
= u
'%s/%s' % (dir_name
, fileName
.replace(suffix
, u
''))
5513 if os
.path
.isfile(file_path
):
5515 if os
.path
.isfile(file_path2
):
5519 file_path
= u
'%s/%s' % (dir_name
, fileName
)
5520 if os
.path
.isfile(file_path
):
5524 # end findFileInDir()
5528 num_secondary_source_graphics_downloaded
=0
5529 num_secondary_source_metadata_downloaded
=0
5531 def processMythTvMetaData(self
):
5532 '''Check each video file in the mythvideo directories download graphics files and meta data then
5533 update MythTV data base meta data with any new information.
5535 # Verify that the proper fields are present
5536 db_version
= mythdb
.settings
.NULL
.DBSchemaVer
5537 field_names
= mythvideo
.tablefields
['videometadata']
5538 for field
in ['season', 'episode', 'coverfile', 'screenshot', 'banner', 'fanart']:
5539 if not field
in field_names
:
5540 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
))
5543 # Initailize and instance to the TMDB api
5544 apikey
= "c27cb71cff5bd76e1a7a009380562c62"
5545 if self
.config
['interactive']:
5546 # themoviedb.org api key given by Travis Bell for Mythtv
5547 self
.config
['tmdb_api'] = tmdb_api
.MovieDb(apikey
,
5550 select_first
= False,
5551 debug
= self
.config
['debug_enabled'],
5553 language
= self
.config
['local_language'],
5554 search_all_languages
= True,)
5556 self
.config
['tmdb_api'] = tmdb_api
.MovieDb(apikey
,
5558 interactive
= False,
5559 select_first
= False,
5560 debug
= self
.config
['debug_enabled'],
5561 language
= self
.config
['local_language'],
5562 search_all_languages
= True,)
5564 # If there were directories specified move them and update the MythTV db meta data accordingly
5565 if self
.config
['video_dir']:
5566 if len(self
.config
['video_dir']) % 2 == 0:
5567 validFiles
= self
._moveVideoFiles
(self
.config
['video_dir'])
5568 self
.config
[u
'file_move_flag'] = False
5570 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']))
5573 # Check if only missing inetref video's should be processed
5574 if self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']:
5575 validFiles
= self
._findMissingInetref
()
5576 if validFiles
== None:
5577 sys
.stderr
.write(u
"\n! Warning: There were no missing interef video files found.\n\n")
5579 elif not len(validFiles
):
5580 sys
.stderr
.write(u
"\n! Warning: There were no missing interef video files found.\n\n")
5583 # Check if this is a Scheduled and Recorded graphics download request
5584 if self
.config
['mythtv_watched']:
5585 self
._downloadScheduledRecordedGraphics
()
5588 # Check if this is just a Janitor (clean up unused graphics files) request
5589 if self
.config
['mythtvjanitor']:
5590 self
._graphicsCleanup
()
5593 directories
=self
.config
['mythvideo']
5595 if not len(directories
):
5596 sys
.stderr
.write(u
"\n! Error: There must be a video directory specified in MythTv\n")
5601 num_fanart_downloads
=0
5602 num_posters_downloads
=0
5603 num_banners_downloads
=0
5604 num_episode_metadata_downloads
=0
5605 num_movies_using_imdb_numbers
=0
5606 num_symlinks_created
=0
5607 num_mythdb_updates
=0
5608 num_posters_below_min_size
=0
5609 videos_with_small_posters
=[]
5610 videos_using_imdb_numbers
=[]
5611 videos_updated_metadata
=[]
5614 sys
.stdout
.write(u
'Mythtv video database maintenance start: %s\n' % (datetime
.datetime
.now()).strftime("%Y-%m-%d %H:%M"))
5616 if not self
.config
['video_dir'] and not self
.config
['mythtv_inetref'] and not self
.config
['mythtv_ref_num']:
5617 allFiles
= self
._findFiles
(directories
, self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
5618 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'], movies
=True)
5620 if not len(validFiles
):
5621 sys
.stderr
.write(u
"\n! Error: No valid video files found\n")
5624 tv_series_season_format
=u
"%s/%s Season %d.%s"
5625 tv_series_format
=u
"%s/%s.%s"
5626 for cfile
in validFiles
:
5627 self
._displayMessage
(u
"\nNow processing video file (%s)(%s)(%s)\n" % (cfile
['filename'], cfile
['seasno'], cfile
['epno']))
5630 videopath
= tv_series_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
5631 # Find the MythTV meta data
5632 result
= mythvideo
.getVideo(exactfile
=videopath
)
5636 intid
= result
.intid
5638 result
= mythvideo
.getVideo(exactfile
=self
.rtnRelativePath(videopath
, u
'mythvideo'), host
=localhostname
.lower())
5641 has_metadata
= False
5643 intid
= result
.intid
5644 if result
.category
== 'none' and result
.year
== 1895:
5645 has_metadata
= False
5649 if result
.category
== 'none' and result
.year
== 1895:
5650 has_metadata
= False
5655 # Unless explicitly requested with options -MI or -MG do not add missing videos to DB
5656 if not self
.config
['interactive'] and not self
.config
['mythtv_guess']:
5658 # Create a new empty entry
5659 sys
.stdout
.write(u
"\n\nEntry does not exist in MythDB. Adding (%s).\n" % cfile
['filename'])
5660 new_rec
= {'title': cfile['file_seriesname'], 'filename': self.rtnRelativePath(videopath, u'mythvideo')}
5661 videopath
= self
.rtnRelativePath(videopath
, u
'mythvideo')
5662 if videopath
[0] == '/':
5663 intid
= Video(db
=mythvideo
).create(new_rec
).intid
5665 new_rec
['host'] = localhostname
.lower()
5666 intid
= Video(db
=mythvideo
).create(new_rec
).intid
5667 elif not has_metadata
:
5668 sys
.stdout
.write(u
"\n\nEntry exists in MythDB but category is 0 and year is 1895 (default values).\nUpdating (%s).\n" % cfile
['filename'])
5669 filename
= self
.rtnRelativePath(videopath
, u
'mythvideo')
5670 if filename
[0] == u
'/':
5671 Video(intid
, db
=mythvideo
).update({'filename': filename, u'host': u''}
)
5673 Video(intid
, db
=mythvideo
).update({'filename': filename, u'host': localhostname.lower()}
)
5674 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
5679 # Get a dictionary of the existing meta data plus a copy for update comparison
5681 vim
= Video(intid
, db
=mythvideo
)
5682 for key
in vim
.keys():
5683 meta_dict
[key
] = vim
[key
]
5685 # Fix a metadata record that has an incorrectly initialized inetref number value
5686 if meta_dict
['inetref'] == None:
5687 meta_dict
['inetref'] = u
'00000000'
5688 available_metadata
= dict(meta_dict
)
5690 available_metadata
['season']=cfile
['seasno']
5691 available_metadata
['episode']=cfile
['epno']
5693 if available_metadata
['title'] == u
'':
5694 available_metadata
['title'] = cfile
['file_seriesname']
5696 # Set whether a video file is stored in a Storage Group or not
5697 if available_metadata
['filename'][0] == u
'/':
5698 self
.absolutepath
= True
5700 self
.absolutepath
= False
5702 # There must be an Internet reference number. Get one for new records.
5703 if _can_int(meta_dict
['inetref']) and not meta_dict
['inetref'] == u
'00000000' and not meta_dict
['inetref'] == '':
5704 if meta_dict
['inetref'] == '99999999': # Records that are not updated by Jamu
5706 inetref
= meta_dict
['inetref']
5707 cfile
['inetref'] = meta_dict
['inetref']
5710 if not self
.config
['interactive'] and not self
.config
['mythtv_guess']:
5711 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']))
5713 inetref
= self
._getTmdbIMDB
(available_metadata
['title'])
5714 cfile
['inetref'] = inetref
5716 self
._displayMessage
(u
"themoviedb.com does not recognize the movie (%s) - Cannot update metadata - skipping\n" % available_metadata
['title'])
5717 missing_inetref
.append(available_metadata
['title'])
5719 # Only update the reference number
5720 if self
.config
['mythtv_ref_num'] or inetref
== '99999999':
5721 Video(intid
, db
=mythvideo
).update({'inetref': inetref}
)
5722 num_mythdb_updates
+=1
5723 videos_updated_metadata
.append(cfile
['filename'])
5724 self
._displayMessage
(u
"\nReference number (%s) added for (%s) \n" % (inetref
, cfile
['filename']))
5728 for key
in available_metadata
.keys():
5729 copy
[key
] = available_metadata
[key
]
5730 tmp_dict
= self
._getTvdbMetadata
(cfile
, copy
)
5732 self
._displayMessage
(u
"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile
['seasno'], cfile
['epno'], videopath
))
5733 missing_inetref
.append(available_metadata
['title'])
5735 inetref
= tmp_dict
['inetref']
5736 available_metadata
['title'] = tmp_dict
['title']
5737 cfile
['file_seriesname'] = tmp_dict
['title']
5738 # Only update the reference number and title
5739 if self
.config
['mythtv_ref_num'] or inetref
== '99999999':
5740 if inetref
== u
'99999999':
5741 Video(intid
, db
=mythvideo
).update({'inetref': inetref}
)
5743 Video(intid
, db
=mythvideo
).update({'inetref': inetref, 'title': tmp_dict['title']}
)
5744 num_mythdb_updates
+=1
5745 videos_updated_metadata
.append(cfile
['filename'])
5746 self
._displayMessage
(u
"\nReference number (%s) added for (%s) \n" % (inetref
, cfile
['filename']))
5748 cfile
['inetref'] = inetref
5749 available_metadata
['inetref'] = inetref
5751 if (meta_dict
['subtitle'] == None or meta_dict
['subtitle'] == '') and not movie
:
5752 tmp_subtitle
= self
._getSubtitle
(cfile
)
5753 if tmp_subtitle
== None:
5754 self
._displayMessage
(u
"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile
['seasno'], cfile
['epno'], videopath
))
5757 available_metadata
['subtitle'] = tmp_subtitle
5758 available_metadata
['title'] = self
.config
['series_name']
5759 cfile
['file_seriesname'] = self
.config
['series_name']
5761 # Check if current inetref is a IMDB#
5762 # If so then check it can be changed to tmdb#
5763 # If it can be changed then rename any graphics and update meta data
5764 if movie
and len(inetref
) == 7:
5765 self
._displayMessage
(u
"%s has IMDB# (%s)" % (available_metadata
['title'], inetref
))
5766 num_movies_using_imdb_numbers
+=1
5767 videos_using_imdb_numbers
.append(u
"%s has IMDB# (%s)" % (available_metadata
['title'], inetref
))
5768 movie_data
= self
._getTmdbMetadata
(cfile
, dict(available_metadata
))
5769 if movie_data
.has_key('inetref'):
5770 if available_metadata
['inetref'] != movie_data
['inetref']:
5771 available_metadata
['inetref'] = movie_data
['inetref']
5772 inetref
= movie_data
['inetref']
5773 cfile
['inetref'] = movie_data
['inetref']
5774 for graphic_type
in ['coverfile', 'banner', 'fanart']: # Rename graphics files
5775 if available_metadata
[graphic_type
] == None or available_metadata
[graphic_type
] == '':
5777 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5778 if os
.path
.isfile(graphic_file
):
5779 filepath
, filename
= os
.path
.split(graphic_file
)
5780 filename
, ext
= os
.path
.splitext( filename
)
5782 if self
.config
['simulation']:
5784 u
"Simulation renaming (%s) to (%s)\n" % (graphic_file
, tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
))
5787 dest
= tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
)
5789 if not os
.path
.isfile(dest
):
5790 os
.rename(graphic_file
, dest
)
5793 u
"Renaming image file (%s) to (%s) failed, error(%s)\n" % (graphic_file
, dest
, e
))
5795 self
._displayMessage
(u
"Renamed (%s) to (%s)\n" % (graphic_file
, tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
)))
5796 available_metadata
[graphic_type
]= self
.rtnRelativePath(dest
, graphicsDirectories
[graphic_type
])
5798 ###############################################################################
5799 # START of metadata Graphics logic - Checking, downloading, renaming
5800 ###############################################################################
5801 for graphic_type
in ['coverfile', 'banner', 'fanart']:
5802 ###############################################################################
5803 # START of MOVIE graphics updating
5804 ###############################################################################
5805 # Check that there are local graphics path for abs path video
5806 # An abs path video can only use the FE specified graphic directories
5807 if self
.absolutepath
:
5808 if not len(self
.config
['localpaths'][graphicsDirectories
[graphic_type
]]):
5810 graphicsdirs
= self
.config
['localpaths'][graphicsDirectories
[graphic_type
]]
5812 graphicsdirs
= self
.config
[graphicsDirectories
[graphic_type
]]
5814 if graphic_type
== 'banner':
5816 if graphic_type
== 'coverfile':
5821 undersized_graphic
= False
5822 for ext
in self
.image_extensions
:
5823 for graphicsdir
in graphicsdirs
:
5824 filename
= self
.findFileInDir(u
"%s.%s" % (inetref
, ext
), [graphicsdir
], suffix
=self
.graphic_suffix
[graphic_type
])
5827 available_metadata
[graphic_type
]=self
.rtnRelativePath(filename
, graphicsDirectories
[graphic_type
])
5828 if graphic_type
== 'coverfile':
5830 (width
, height
) = self
.config
['image_library'].open(filename
).size
5831 if width
< self
.config
['min_poster_size']:
5832 num_posters_below_min_size
+=1
5833 videos_with_small_posters
.append(cfile
['filename'])
5834 undersized_graphic
= True
5837 undersized_graphic
= True
5839 need_graphic
= False
5841 if not need_graphic
:
5844 if need_graphic
== True:
5845 dummy_graphic
= self
._getTmdbGraphics
(cfile
, g_type
)
5847 # Try secondary source if themoviedb.com did not have graphicrecord['title']
5848 if dummy_graphic
== None or undersized_graphic
== True:
5849 dummy_graphic
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5851 if dummy_graphic
!= None:
5852 available_metadata
[graphic_type
] = self
.rtnRelativePath(dummy_graphic
, graphicsDirectories
[graphic_type
])
5853 if graphic_type
== 'fanart':
5854 self
._displayMessage
(u
"Movie - Added fan art for(%s)" % cfile
['filename'])
5855 num_fanart_downloads
+=1
5857 self
._displayMessage
(u
"Movie - Added a poster for(%s)" % cfile
['filename'])
5858 num_posters_downloads
+=1
5860 # END of Movie graphics updates ###############################################
5862 ###############################################################################
5863 # START of TV Series graphics updating
5864 ###############################################################################
5865 need_graphic
= False
5866 new_format
= True # Initialize that a graphics file NEEDS a new format
5867 # Check if an existing TV series graphic file is in the old naming format
5868 if available_metadata
[graphic_type
] != None and available_metadata
[graphic_type
] != 'No Cover' and available_metadata
[graphic_type
] != '':
5869 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5870 filepath
, filename
= os
.path
.split(graphic_file
)
5871 filename
, ext
= os
.path
.splitext( filename
)
5872 if filename
.find(u
' Season ') != -1:
5876 if need_graphic
or new_format
: # Graphic does not exist or is in an old format
5877 for ext
in self
.image_extensions
:
5878 for graphicsdir
in graphicsdirs
:
5879 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)
5881 available_metadata
[graphic_type
]=self
.rtnRelativePath(filename
, graphicsDirectories
[graphic_type
])
5882 need_graphic
= False
5883 if graphic_type
== 'coverfile':
5885 (width
, height
) = self
.config
['image_library'].open(filename
).size
5886 if width
< self
.config
['min_poster_size']:
5887 num_posters_below_min_size
+=1
5888 videos_with_small_posters
.append(cfile
['filename'])
5891 undersized_graphic
= True
5894 if not need_graphic
:
5897 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5898 if not graphic_file
== None:
5899 graphic_file
= self
.findFileInDir(graphic_file
, [graphicsdir
], suffix
=self
.graphic_suffix
[graphic_type
], fuzzy_match
=True)
5900 if graphic_file
== None:
5902 if not need_graphic
: # Have graphic but may be using an old naming convention
5904 season_missing
= False
5905 suffix_missing
= False
5906 if graphic_file
.find(u
' Season ') == -1: # Check for Season
5908 season_missing
= True
5909 if graphic_file
.find(self
.graphic_suffix
[graphic_type
]) == -1:
5911 suffix_missing
= True
5913 filepath
, filename
= os
.path
.split(graphic_file
)
5914 baseFilename
, ext
= os
.path
.splitext( filename
)
5915 baseFilename
= self
.sanitiseFileName(baseFilename
)
5916 if season_missing
and suffix_missing
:
5917 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
, available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5918 elif suffix_missing
:
5919 newFilename
= u
"%s/%s%s%s" % (filepath
, baseFilename
, self
.graphic_suffix
[graphic_type
], ext
)
5920 elif season_missing
:
5921 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5922 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5923 if self
.config
['simulation']:
5925 u
"Simulation renaming (%s) to (%s)\n" % (graphic_file
, newFilename
)
5928 os
.rename(graphic_file
, newFilename
)
5929 available_metadata
[graphic_type
]= self
.rtnRelativePath(newFilename
, graphicsDirectories
[graphic_type
])
5931 available_metadata
[graphic_type
]= self
.rtnRelativePath(graphic_file
, graphicsDirectories
[graphic_type
])
5932 else: # Must see if a graphic is on thetvdb wiki
5933 if graphic_type
== 'coverfile' or graphic_type
== 'banner':
5934 available_metadata
[graphic_type
] = self
.rtnRelativePath(self
._getTvdbGraphics
(cfile
, graphic_type
), graphicsDirectories
[graphic_type
])
5935 if available_metadata
[graphic_type
] == None:
5936 tmp
= self
._getTvdbGraphics
(cfile
, graphic_type
, toprated
=True)
5938 tmp_fullfilename
= self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
])
5939 filepath
, filename
= os
.path
.split(tmp_fullfilename
)
5940 baseFilename
, ext
= os
.path
.splitext( filename
)
5941 baseFilename
= self
.sanitiseFileName(baseFilename
)
5942 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5943 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5944 if self
.config
['simulation']:
5946 u
"Simulation rename (%s) to (%s)\n" % (tmp_fullfilename
,newFilename
)
5949 self
._displayMessage
(u
"Rename existing graphic %s for series (%s)" % (graphic_type
, available_metadata
['title']))
5951 os
.rename(tmp_fullfilename
, newFilename
)
5952 if graphic_type
== 'coverfile':
5953 self
._displayMessage
("1-Added a poster for(%s)" % cfile
['filename'])
5954 num_posters_downloads
+=1
5956 self
._displayMessage
("1-Added a banner for(%s)" % cfile
['filename'])
5957 num_banners_downloads
+=1
5958 available_metadata
[graphic_type
] = self
.rtnRelativePath(newFilename
, graphicsDirectories
[graphic_type
])
5961 u
"IOError coping (%s) to (%s)\nError:(%s)\n" % (tmp_fullfilename
, newFilename
, e
))
5962 else: # Try a secondary source
5963 dummy
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5965 if graphic_type
== 'coverfile':
5966 self
._displayMessage
(u
"1-Secondary source poster for(%s)" % cfile
['filename'])
5967 num_posters_downloads
+=1
5969 self
._displayMessage
(u
"1-Secondary source banner for(%s)" % cfile
['filename'])
5970 num_banners_downloads
+=1
5971 available_metadata
[graphic_type
] = self
.rtnRelativePath(dummy
, graphicsDirectories
[graphic_type
])
5972 else: # download fanart
5973 tmp
= self
.rtnAbsolutePath(self
._getTvdbGraphics
(cfile
, graphic_type
, toprated
=True), graphicsDirectories
['fanart'])
5975 filepath
, filename
= os
.path
.split(tmp
)
5976 baseFilename
, ext
= os
.path
.splitext( filename
)
5977 baseFilename
= self
.sanitiseFileName(baseFilename
)
5978 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5979 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5980 if self
.config
['simulation']:
5982 u
"Simulation fanart rename (%s) to (%s)\n" % (tmp
, newFilename
)
5986 os
.rename(self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
]), newFilename
)
5987 available_metadata
['fanart'] = self
.rtnRelativePath(newFilename
, graphicsDirectories
['fanart'])
5988 num_fanart_downloads
+=1
5991 u
"IOError coping (%s) to (%s)\nError:(%s)\n" % (self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
]), newFilename
, e
))
5992 else: # Try a secondary source
5993 dummy
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5995 available_metadata
['fanart'] = self
.rtnRelativePath(dummy
, graphicsDirectories
['fanart'])
5996 num_fanart_downloads
+=1
5997 # END of TV Series graphics updating
5998 ###############################################################################
5999 # END of metadata Graphics logic - Checking, downloading, renaming
6000 ###############################################################################
6002 ###############################################################################
6003 # START of metadata text logic - Checking, downloading, renaming
6004 ###############################################################################
6005 # Clean up meta data code
6007 if available_metadata
['rating'] == 'TV Show':
6008 available_metadata
['rating'] = 'NR'
6010 # Check if any meta data needs updating
6011 metadata_update
= True
6012 for key
in available_metadata
.keys():
6013 if key
in self
.config
['metadata_exclude_as_update_trigger']:
6016 if key
== 'rating' and (available_metadata
[key
] == 'NR' or available_metadata
[key
] == '' or available_metadata
[key
] == 'Unknown'):
6017 self
._displayMessage
(
6018 u
"At least (%s) needs updating\n" % (key
))
6020 if key
== 'userrating' and available_metadata
[key
] == 0.0:
6021 self
._displayMessage
(
6022 u
"At least (%s) needs updating\n" % (key
))
6024 if key
== 'length' and available_metadata
[key
] == 0:
6025 self
._displayMessage
(
6026 u
"At least (%s) needs updating\n" % (key
))
6028 if key
== 'category' and available_metadata
[key
] == 0:
6029 self
._displayMessage
(
6030 u
"At least (%s) needs updating\n" % (key
))
6032 if key
== 'year' and (available_metadata
[key
] == 0 or available_metadata
[key
] == 1895):
6033 self
._displayMessage
(
6034 u
"At least (%s) needs updating\n" % (key
))
6036 if movie
and key
== 'subtitle': # There are no subtitles in movies
6038 if movie
and key
== 'plot' and available_metadata
[key
] != None:
6039 if len(available_metadata
[key
].split(' ')) < 10:
6040 self
._displayMessage
(
6041 u
"The plot is less than 10 words check if a better plot exists\n")
6043 if key
== 'releasedate' and (available_metadata
[key
] == None or available_metadata
[key
] == date(1,1,1)):
6044 self
._displayMessage
(
6045 u
"At least (%s) needs updating\n" % (key
))
6047 if key
== 'hash' and (available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None):
6048 if (os
.path
.getsize(u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])) < 65536 * 2):
6050 self
._displayMessage
(
6051 u
"At least (%s) needs updating\n" % (key
))
6053 if available_metadata
[key
] == None or available_metadata
[key
] == '' or available_metadata
[key
] == 'None' or available_metadata
[key
] == 'Unknown':
6054 self
._displayMessage
(
6055 u
"At least (%s) needs updating\n" % (key
))
6058 metadata_update
= False
6059 if not movie
and not len(available_metadata
['inetref']) >= 5:
6060 self
._displayMessage
(
6061 u
"At least (%s) needs updating\n" % ('inetref'))
6062 metadata_update
= True
6063 # Find the video file's real duration in minutes
6065 length
= self
._getVideoLength
(u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'], ))
6069 if length
!= available_metadata
['length']:
6070 self
._displayMessage
(u
"Video file real length (%d) minutes needs updating\n" % (length
))
6071 metadata_update
= True
6074 genres_cast
={'genres': u'', 'cast': u''}
6076 copy
= dict(available_metadata
)
6078 tmp_dict
= self
._getTmdbMetadata
(cfile
, copy
)
6080 tmp_dict
= self
._getTvdbMetadata
(cfile
, copy
)
6081 num_episode_metadata_downloads
+=1
6084 for key
in ['genres', 'cast', 'countries']:
6085 if tmp_dict
.has_key(key
):
6086 genres_cast
[key
] = tmp_dict
[key
]
6087 for key
in available_metadata
.keys():
6088 if key
in self
.config
['metadata_exclude_as_update_trigger']:
6091 if not tmp_dict
.has_key(key
):
6093 if key
== 'userrating' and available_metadata
[key
] == 0.0:
6094 available_metadata
[key
] = tmp_dict
[key
]
6098 length
= self
._getVideoLength
(u
'%s/%s.%s' %(cfile
['filepath'], cfile
['filename'], cfile
['ext'], ))
6102 available_metadata
['length'] = length
6104 available_metadata
[key
] = tmp_dict
[key
]
6106 available_metadata
[key
] = tmp_dict
[key
]
6108 # Fix fields that must be prepared for insertion into data base
6109 available_metadata
['title'] = self
._make
_db
_ready
(available_metadata
['title'])
6110 available_metadata
['director'] = self
._make
_db
_ready
(available_metadata
['director'])
6111 available_metadata
['plot'] = self
._make
_db
_ready
(available_metadata
['plot'])
6112 if available_metadata
['year'] == 0:
6113 available_metadata
['year'] = 1895
6114 if available_metadata
['coverfile'] == None:
6115 available_metadata
['coverfile'] = u
'No Cover'
6116 if len(genres_cast
['genres']) and available_metadata
['category'] == 'none':
6118 genres
= genres_cast
['genres'][:genres_cast
['genres'].index(',')]
6120 genres
= genres_cast
['genres']
6121 available_metadata
['category'] = genres
6122 self
._displayMessage
(u
"Category added for (%s)(%s)" % (available_metadata
['title'], available_metadata
['category']))
6124 # Make sure graphics relative/absolute paths are set PROPERLY based
6125 # on the 'filename' field being a relative or absolute path. A filename with an absolite path
6126 # CAN ONLY have graphics baed on absolute paths.
6127 # A filename with a relative path can have mixed absolute and relative path graphic files
6128 if available_metadata
[u
'filename'][0] == u
'/':
6129 available_metadata
[u
'host'] = u
''
6130 for key
in [u
'coverfile', u
'banner', u
'fanart']:
6131 if available_metadata
[key
] != None and available_metadata
[key
] != u
'No Cover' and available_metadata
[key
] != u
'':
6132 if available_metadata
[key
][0] != u
'/':
6133 tmp
= self
.rtnAbsolutePath(available_metadata
[key
], graphicsDirectories
[key
])
6135 if key
== u
'coverfile':
6136 available_metadata
[key
] = u
'No Cover'
6138 available_metadata
[key
] = u
''
6140 available_metadata
[u
'host'] = localhostname
.lower()
6142 ###############################################################################
6143 # END of metadata text logic - Checking, downloading, renaming
6144 ###############################################################################
6146 ###############################################################################
6147 # START of metadata updating the MythVideo record when graphics or text has changed
6148 ###############################################################################
6149 # Check if any new information was found
6150 if not self
.config
['overwrite']:
6151 for key
in available_metadata
.keys():
6152 if available_metadata
[key
] != meta_dict
[key
]:
6153 if available_metadata
[key
] == u
'' and meta_dict
[key
] == None:
6155 if available_metadata
[key
] == u
'' and meta_dict
[key
] == u
'Unknown':
6158 self
._displayMessage
(
6159 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
])))
6161 self
._displayMessage
(
6162 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
])))
6165 self
._displayMessage
(
6166 u
"Nothing to update for video file(%s)\n" % cfile
['filename']
6170 if self
.config
['simulation']:
6172 u
"Simulation MythTV DB update for video file(%s)\n" % cfile
['filename']
6174 for key
in available_metadata
.keys():
6175 print key
," ", available_metadata
[key
]
6176 for key
in genres_cast
.keys():
6177 sys
.stdout
.write(u
"Key(%s):(%s)\n" % (key
, genres_cast
[key
]))
6179 sys
.stdout
.write('\n')
6181 # Clean up a few fields before updating Mythdb
6182 if available_metadata
['showlevel'] == 0: # Allows mythvideo to display this video
6183 available_metadata
['showlevel'] = 1
6184 Video(intid
, db
=mythvideo
).update(available_metadata
)
6185 num_mythdb_updates
+=1
6186 videos_updated_metadata
.append(cfile
['filename'])
6187 for key
in ['genres', 'cast', 'countries']:
6188 if key
== 'genres' and len(cfile
['categories']):
6189 genres_cast
[key
]+=cfile
['categories']
6190 if genres_cast
.has_key(key
):
6191 self
._addCastGenreCountry
( genres_cast
[key
], Video(intid
, db
=mythvideo
), key
)
6192 self
._displayMessage
(
6193 u
"Updated Mythdb for video file(%s)\n" % cfile
['filename']
6195 ###############################################################################
6196 # END of metadata updating the MythVideo record when graphics or text has changed
6197 ###############################################################################
6199 sys
.stdout
.write(u
"\nMythtv video database maintenance ends at : %s\n" % (datetime
.datetime
.now()).strftime("%Y-%m-%d %H:%M"))
6202 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
))
6204 if len(videos_updated_metadata
):
6205 sys
.stdout
.write(u
'\n--------------Updated Video Files----------\n' )
6206 for videofile
in videos_updated_metadata
:
6207 sys
.stdout
.write(u
'%s\n' % videofile
)
6208 if len(missing_inetref
):
6209 sys
.stdout
.write(u
'\n----------------No Inetref Found-----------\n' )
6210 for videofile
in missing_inetref
:
6211 sys
.stdout
.write(u
'%s\n' % videofile
)
6212 if len(videos_with_small_posters
):
6213 sys
.stdout
.write(u
'\n---------------Under sized Poster----------\n' )
6214 for videofile
in videos_with_small_posters
:
6215 sys
.stdout
.write(u
'%s\n' % videofile
)
6216 if len(videos_using_imdb_numbers
):
6217 sys
.stdout
.write(u
'\n---------------Movies with IMDB#s----------\n' )
6218 for videofile
in videos_using_imdb_numbers
:
6219 sys
.stdout
.write(u
'%s\n' % videofile
)
6221 # end processMythTvMetaData
6223 def __repr__(self
): # Just a place holder
6227 # end MythTvMetaData
6229 def simple_example():
6230 """Simple example of using jamu
6231 Displays the poster graphics URL(s) and episode meta data for the TV series Sanctuary, season 1
6233 returns None if there was no data found for the request TV series
6234 returns False if there is no TV series as specified
6235 returns a string with poster URLs and episode meta data
6237 # Get an instance of the variable configuration information set to default values
6238 configuration
= Configuration(interactive
= True, debug
= False)
6240 # Set the type of data to be returned
6241 configuration
.changeVariable('get_poster', True)
6242 configuration
.changeVariable('get_ep_meta', True)
6244 # Validate specific variables and set the TV series information
6245 configuration
.validate_setVariables(['Sanctuary', '1', '3'])
6247 # Get an instance of the tvdb process function and fetch the data
6248 process
= Tvdatabase(configuration
.config
)
6249 results
= process
.processTVdatabaseRequests()
6251 if results
!= None and results
!= False: # Print the returned data string to the stdout
6252 print process
.processTVdatabaseRequests().encode('utf8')
6253 # end simple_example
6257 """Support jamu from the command line
6260 parser
= OptionParser(usage
=u
"%prog usage: jamu -hbueviflstdnmoCRFUDSGN [parameters]\n <series name/SID or 'series/SID and season number' or 'series/SID and season number and episode number' or 'series/SID and episode name' or video file/directory paired with destination directory'>")
6262 parser
.add_option( "-b", "--debug", action
="store_true", default
=False, dest
="debug",
6263 help=u
"Show debugging info")
6264 parser
.add_option( "-u", "--usage", action
="store_true", default
=False, dest
="usage",
6265 help=u
"Display the six main uses for this jamu")
6266 parser
.add_option( "-e", "--examples", action
="store_true", default
=False, dest
="examples",
6267 help=u
"Display examples for executing the jamu script")
6268 parser
.add_option( "-v", "--version", action
="store_true", default
=False, dest
="version",
6269 help=u
"Display version and author information")
6270 parser
.add_option( "-i", "--interactive", action
="store_true", default
=False, dest
="interactive",
6271 help=u
"Interactive mode allows selection of a specific Series from a series list")
6272 parser
.add_option( "-f", "--flags_options", action
="store_true", default
=False,dest
="flags_options",
6273 help=u
"Display all variables and settings then exit")
6274 parser
.add_option( "-l", "--language", metavar
="LANGUAGE", default
=u
'en', dest
="language",
6275 help=u
"Select data that matches the specified language fall back to english if nothing found (e.g. 'es' Español, 'de' Deutsch ... etc)")
6276 parser
.add_option( "-s", "--simulation", action
="store_true", default
=False, dest
="simulation",
6277 help=u
"Simulation (dry run), no downloads are performed or data bases altered")
6278 parser
.add_option( "-t", "--toprated", action
="store_true", default
=False, dest
="toprated",
6279 help=u
"Only display/download the top rated TV Series graphics")
6280 parser
.add_option( "-d", "--download", action
="store_true", default
=False, dest
="download",
6281 help=u
"Download and save the graphics and/or meta data")
6282 parser
.add_option( "-n", "--nokeys", action
="store_true", default
=False, dest
="nokeys",
6283 help=u
"Do not add data type keys to data values when displaying data")
6284 parser
.add_option( "-m", "--maximum", metavar
="MAX", default
=None, dest
="maximum",
6285 help=u
"Limit the number of graphics per type downloaded. e.g. --maximum=6")
6286 parser
.add_option( "-o", "--overwrite", action
="store_true", default
=False, dest
="overwrite",
6287 help=u
"Overwrite any matching files already downloaded")
6288 parser
.add_option( "-C", "--user_config", metavar
="FILE", default
="", dest
="user_config",
6289 help=u
"User specified configuration variables. e.g --user_config='~/.jamu/jamu.conf'")
6290 parser
.add_option( "-F", "--filename", action
="store_true", default
=False, dest
="ret_filename",
6291 help=u
"Display a formated filename for an episode")
6292 parser
.add_option( "-U", "--update", action
="store_true", default
=False, dest
="update",
6293 help=u
"Update a meta data file if local episode meta data is older than what is available on thetvdb.com")
6294 parser
.add_option( "-D", "--mythtvdir", action
="store_true", default
=False, dest
="mythtvdir",
6295 help=u
"Store graphic files into the MythTV DB specified dirs")
6296 parser
.add_option( "-M", "--mythtvmeta", action
="store_true", default
=False, dest
="mythtvmeta",
6297 help=u
"Add/update TV series episode or movie meta data in MythTV DB")
6298 parser
.add_option( "-V", "--mythtv_verbose", action
="store_true", default
=False, dest
="mythtv_verbose",
6299 help=u
"Display verbose messages when performing MythTV metadata maintenance")
6300 parser
.add_option( "-J", "--mythtvjanitor", action
="store_true", default
=False, dest
="mythtvjanitor",
6301 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.")
6302 parser
.add_option( "-N", "--mythtvNFS", action
="store_true", default
=False, dest
="mythtvNFS",
6303 help=u
"This option overrides Jamu's restrictions on processing NFS mounted Video and/or graphic files.")
6304 parser
.add_option( "-I", "--mythtv_inetref", action
="store_true", default
=False, dest
="mythtv_inetref",
6305 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.")
6306 parser
.add_option( "-W", "--mythtv_watched", action
="store_true", default
=False, dest
="mythtv_watched",
6307 help=u
"Download graphics for Scheduled and Recorded videos. This option is ONLY active if the -M option is also selected.")
6308 parser
.add_option( "-G", "--mythtv_guess", action
="store_true", default
=False, dest
="mythtv_guess",
6309 help=u
"Guess at the inetref for a video. This option is ONLY active if the -M option is also selected.")
6310 parser
.add_option( "-S", "--selected_data", metavar
="TYPES", default
=None, dest
="selected_data",
6311 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")
6312 parser
.add_option( "-R", "--mythtv_ref_num", action
="store_true", default
=False, dest
="mythtv_ref_num",
6313 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.")
6315 opts
, series_season_ep
= parser
.parse_args()
6319 print "\nargs", series_season_ep
6321 # Set the default configuration values
6322 if opts
.mythtv_inetref
or opts
.mythtv_ref_num
:
6323 opts
.interactive
= True
6324 configuration
= Configuration(interactive
= opts
.interactive
, debug
= opts
.debug
)
6326 if opts
.usage
: # Display usage information
6327 sys
.stdout
.write(usage_txt
+'\n')
6330 if opts
.examples
: # Display example information
6331 sys
.stdout
.write(examples_txt
+'\n')
6334 if opts
.version
== True: # Display program information
6335 sys
.stdout
.write(u
"\nTitle: (%s); Version: (%s); Author: (%s)\n%s\n" % (
6336 __title__
, __version__
, __author__
, __purpose__
))
6339 # Verify that only one instance of the following options is running at any one time
6340 # Options (-M, -MW and -MG)
6345 MythLog
._setlevel
('none') # There cannot be any logging messages with non -M options
6346 if opts
.mythtvmeta
and opts
.mythtv_watched
:
6348 if opts
.mythtvmeta
and opts
.mythtv_guess
:
6350 if opts
.mythtvmeta
and opts
.mythtvjanitor
: # No instance check with the janitor option
6352 if opts
.mythtvmeta
and opts
.mythtv_inetref
: # No instance check with the interactive mode option
6354 if options
in [u
'M', u
'MW', u
'MG']:
6355 jamu_instance
= singleinstance(u
'/tmp/Jamu_%s_instance.pid' % options
)
6357 # check is another instance of Jamu is running
6359 if jamu_instance
.alreadyrunning():
6360 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
6363 # Message the user that they are using incompatible options with the -MW option
6364 if opts
.mythtvmeta
and opts
.mythtv_watched
and (opts
.mythtv_inetref
or opts
.interactive
):
6365 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'
6368 # Message the user that they are using incompatible options -R and -I or -i
6369 if opts
.mythtvmeta
and opts
.mythtv_ref_num
and opts
.mythtv_inetref
:
6370 print u
'\n! Error: The (-R) and (-I) options are mutually exclusive.\nPlease change your options and try again.\n'
6373 # Apply any command line switches
6374 configuration
.changeVariable('local_language', opts
.language
)
6375 configuration
.changeVariable('simulation', opts
.simulation
)
6376 configuration
.changeVariable('toprated', opts
.toprated
)
6377 configuration
.changeVariable('download', opts
.download
)
6378 configuration
.changeVariable('nokeys', opts
.nokeys
)
6379 configuration
.changeVariable('maximum', opts
.maximum
)
6380 configuration
.changeVariable('overwrite', opts
.overwrite
)
6381 configuration
.changeVariable('ret_filename', opts
.ret_filename
)
6382 configuration
.changeVariable('update', opts
.update
)
6383 configuration
.changeVariable('mythtvdir', opts
.mythtvdir
)
6384 configuration
.changeVariable('mythtvmeta', opts
.mythtvmeta
)
6385 configuration
.changeVariable('mythtv_inetref', opts
.mythtv_inetref
)
6386 configuration
.changeVariable('mythtv_ref_num', opts
.mythtv_ref_num
)
6387 configuration
.changeVariable('mythtv_watched', opts
.mythtv_watched
)
6388 configuration
.changeVariable('mythtv_guess', opts
.mythtv_guess
)
6389 configuration
.changeVariable('mythtv_verbose', opts
.mythtv_verbose
)
6390 configuration
.changeVariable('mythtvjanitor', opts
.mythtvjanitor
)
6391 configuration
.changeVariable('mythtvNFS', opts
.mythtvNFS
)
6392 configuration
.changeVariable('data_flags', opts
.selected_data
)
6394 # Check if the user wants to change options via a configuration file
6395 if opts
.user_config
!= '': # Did the user want to override the default config file name/location
6396 configuration
.setUseroptions(opts
.user_config
)
6398 default_config
= u
"%s/%s" % (os
.path
.expanduser(u
"~"), u
".mythtv/jamu.conf")
6399 if os
.path
.isfile(default_config
):
6400 configuration
.setUseroptions(default_config
)
6402 print u
"\nThere was no default Jamu configuration file found (%s)\n" % default_config
6404 if opts
.flags_options
: # Display option variables
6405 if len(series_season_ep
):
6406 configuration
.validate_setVariables(series_season_ep
)
6408 configuration
.validate_setVariables(['FAKE SERIES NAME','FAKE EPISODE NAME'])
6409 configuration
.displayOptions()
6412 # Validate specific variables
6413 configuration
.validate_setVariables(series_season_ep
)
6415 if configuration
.config
['mythtvmeta']:
6416 process
= MythTvMetaData(configuration
.config
)
6417 process
.processMythTvMetaData()
6418 elif configuration
.config
['video_dir']:
6419 process
= VideoFiles(configuration
.config
)
6420 results
= process
.processFileOrDirectory()
6421 if results
!= None and results
!= False:
6422 print process
.processFileOrDirectory().encode('utf8')
6424 process
= Tvdatabase(configuration
.config
)
6425 results
= process
.processTVdatabaseRequests()
6426 if results
!= None and results
!= False:
6427 print process
.processTVdatabaseRequests().encode('utf8')
6431 if __name__
== "__main__":