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.
308 JAMU - Just.Another.Metadata.Utility is a versatile utility for downloading graphics and meta data
309 for both movies and TV Series information from themoviedb.com wiki and thetvdb.com wiki. In addition
310 the MythTV data base is updated with the downloaded information.
311 Here are the main uses for this utility:
312 MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
314 1) Simple command line invocation to display or download data from thetvdb.com.
315 Data can be one or more of: Posters/Cover art, Banners, Fan art,
316 Episode Images and Episode meta data. use the command "jamu -e | less" to see
317 command line examples.
318 2) Mass downloads of data matching your video files. **
319 This typically done once to download the information for your video collection.
320 3) Automated maintenance of the information in your video collection. **
321 4) The creation of video file names which can be used to set the file name of your recorded TV shows.
322 File names can be formated to the users preference with information like series name, season number,
323 episode number and episode name. MythTV users may find this valuable as part of a user job
324 that is spawned automatically by mythbackend when recording is finished.
325 5) Jamu's modules can be imported into your own python scripts to create enhanced functionality.
326 6) With the installation of free ImageMagick's utilities (specifically 'mogrify') you can resize
327 graphics when they are downloaded.
328 7) Update the MythTV data base with links to posters, banners, fanart and episode images and optionally
329 download missing graphics if they exist. This feature can be used for mass updates and regular
335 MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
336 These examples are primarily for non-MythTV users of Jamu.
338 jamu command line examples:
339 NOTE: Included here are simple examples of jamu in action.
340 Please review jamu_README for advise on how to get the most out of jamu.
342 ( Display a TV series top rated poster fanart and banner URLs)
343 > jamu -tS PBF "Sanctuary"
344 poster:http://www.thetvdb.com/banners/posters/80159-1.jpg
345 fanart:http://www.thetvdb.com/banners/fanart/original/80159-2.jpg
346 banner:http://www.thetvdb.com/banners/graphical/80159-g2.jpg
348 ( Display the URL for a TV series episode )
349 > jamu -tS I "Fringe" 1 5
350 filename:http://www.thetvdb.com/banners/episodes/82066-391049.jpg
352 ( Display poster, fanart and banner graphics for a TV series but limited to two per type in a season )
353 > jamu -S PBF -m 2 "24" 4
354 poster:http://www.thetvdb.com/banners/seasons/76290-4-3.jpg
355 poster:http://www.thetvdb.com/banners/seasons/76290-4.jpg
356 fanart:http://www.thetvdb.com/banners/fanart/original/76290-1.jpg
357 fanart:http://www.thetvdb.com/banners/fanart/original/76290-2.jpg
358 banner:http://www.thetvdb.com/banners/seasonswide/76290-4.jpg
359 banner:http://www.thetvdb.com/banners/seasonswide/76290-4-3.jpg
361 ( Display a file name string (less file extention and directory path) for a TV episode )
363 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
365 > jamu -F "24" "Day 4: 9:00 A.M.-10:00 A.M."
366 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
368 ( Using SID number instead of series name )
370 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
372 ( Simulate a dry run for the download of a TV series top rated poster and fanart )
373 > jamu -sdtS PF "Fringe"
374 Simulation download of URL(http://www.thetvdb.com/banners/posters/82066-6.jpg) to File(~/Pictures/Poster - 82066-6.jpg)
375 Get_Poster downloading successfully processed
376 Simulation download of URL(http://www.thetvdb.com/banners/fanart/original/82066-11.jpg) to File(~/Pictures/Fanart - 82066-11.jpg)
377 Get_Fanart downloading successfully processed
379 ( Download the Episode meta data and episode image for a video file whose file name contains the series and season/episode numbers)
380 > jamu -dS EI "~/Pictures/Fringe - S01E01.mkv"
381 Episode meta data and/or images downloads successfully processed
384 60 -rw-r--r-- 1 user user 53567 2009-03-12 22:05 Fringe - S01E01 - Pilot.jpg
385 4 -rw-r--r-- 1 user user 1059 2009-03-12 22:05 Fringe - S01E01 - Pilot.meta
386 4 -rw-r--r-- 1 user user 811 2009-03-12 13:22 Fringe - S01E01.mkv
388 ( Display Episode meta data for a TV series )
393 episodename:Day 5: 9:00 A.M.-10:00 A.M.
395 overview:Jack conceals himself inside the airport hanger and surveys the Russian separatists, feeding information to Curtis and his assault team.
396 The terrorists begin executing hostages in an attempt to make Logan cave into their demands.
397 Martha discovers that all traces of her conversation with Palmer may not have been erased.
400 gueststars:John Gleeson Connolly, V.J. Foster, David Dayan Fisher, Taylor Nichols, Steve Edwards, Taras Los, Joey Munguia, Reggie Jordan, Lou Richards, Karla Zamudio
402 filename:http://www.thetvdb.com/banners/episodes/76290-306117.jpg
405 firstaired:2006-01-16
406 lastupdated:1197942225
407 productioncode:5AFF03
413 combined_episodenumber:4.0
417 dvd_episodenumber:4.0
419 ( Specify a user defined configuration file to set most of the configuration variables )
420 > jamu -C "~/.jamu/jamu.conf" -S P "Supernatural"
421 poster:http://www.thetvdb.com/banners/posters/78901-3.jpg
422 poster:http://www.thetvdb.com/banners/posters/78901-1.jpg
424 ( Display in alphabetical order the state of all configuration variables )
426 allgraphicsdir (~/Pictures)
430 debug_enabled (False)
432 ... lots of configuration variables ...
434 video_file_exts (['3gp', 'asf', 'asx', 'avi', 'mkv', 'mov', 'mp4', 'mpg', 'qt', 'rm', 'swf', 'wmv', 'm2ts', 'evo', 'ts', 'img', 'iso'])
435 with_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s)
436 without_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s)
440 import sys
, os
, re
, locale
, subprocess
, locale
, ConfigParser
, urllib
, codecs
, shutil
, datetime
, fnmatch
, string
441 from datetime
import date
442 from optparse
import OptionParser
443 from socket
import gethostname
, gethostbyname
444 import tempfile
, struct
447 class OutStreamEncoder(object):
448 """Wraps a stream with an encoder"""
449 def __init__(self
, outstream
, encoding
=None):
452 self
.encoding
= sys
.getfilesystemencoding()
454 self
.encoding
= encoding
456 def write(self
, obj
):
457 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
458 if isinstance(obj
, unicode):
460 self
.out
.write(obj
.encode(self
.encoding
))
469 def __getattr__(self
, attr
):
470 """Delegate everything but write to the stream"""
471 return getattr(self
.out
, attr
)
472 sys
.stdout
= OutStreamEncoder(sys
.stdout
, 'utf8')
473 sys
.stderr
= OutStreamEncoder(sys
.stderr
, 'utf8')
478 print '''The python module xml must be installed. error(%s)''' % e
480 if xml
.__version
__ < u
'41660':
482 \n! Warning - The module xml (v41660 or greater) must be installed. Your version is different (v%s) than what Jamu was tested with. Jamu may not work on your installation.\nIt is recommended that you upgrade.\n''' % xml
.__version
__
483 import xml
.etree
.cElementTree
as ElementTree
486 # Find out if the MythTV python bindings can be accessed and instances can be created
488 '''If the MythTV python interface is found, we can insert data directly to MythDB or
489 get the directories to store poster, fanart, banner and episode graphics.
491 from MythTV
import MythDB
, DBData
, Video
, MythVideo
, MythBE
, FileOps
, MythError
, MythLog
496 '''Create an instance of each: MythDB, MythVideo
498 MythLog
._setlevel
('none') # Some non option -M cannot have any logging on stdout
500 mythvideo
= MythVideo(mythdb
)
501 MythLog
._setlevel
('important,general')
503 print u
'\n! Warning - %s' % e
.args
[0]
504 filename
= os
.path
.expanduser("~")+'/.mythtv/config.xml'
505 if not os
.path
.isfile(filename
):
506 print u
'\n! Warning - A correctly configured (%s) file must exist\n' % filename
508 print u
'\n! Warning - Check that (%s) is correctly configured\n' % filename
510 print u
"\n! Warning - Creating an instance caused an error for one of: MythDBConn or MythVideo, error(%s)\n" % e
512 localhostname
= mythdb
.gethostname()
514 localhostname
= gethostname()
516 MythLog
._setlevel
('none') # Some non option -M cannot have any logging on stdout
517 mythbeconn
= MythBE(backend
=localhostname
, db
=mythdb
)
518 MythLog
._setlevel
('important,general')
520 print u
'\nWith any -M option Jamu must be run on a MythTV backend'
521 print u
'! Warning - %s' % e
.args
[0]
524 print u
"\n! Warning - MythTV python bindings could not be imported, error(%s)\n" % e
530 # Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available
532 # thetvdb.com specific modules
533 import MythTV
.ttvdb
.tvdb_ui
as tvdb_ui
534 # from tvdb_api import Tvdb
535 import MythTV
.ttvdb
.tvdb_api
as tvdb_api
536 from MythTV
.ttvdb
.tvdb_exceptions
import (tvdb_error
, tvdb_shownotfound
, tvdb_seasonnotfound
, tvdb_episodenotfound
, tvdb_episodenotfound
, tvdb_attributenotfound
, tvdb_userabort
)
538 # verify version of tvdbapi to make sure it is at least 1.0
539 if tvdb_api
.__version
__ < '1.0':
540 print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api
.__version
__
544 The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py.
545 They should have been installed along with the MythTV python bindings.
552 import MythTV
.tmdb
.tmdb_api
as tmdb_api
553 from MythTV
.tmdb
.tmdb_exceptions
import (TmdBaseError
, TmdHttpError
, TmdXmlError
, TmdbUiAbort
, TmdbMovieOrPersonNotFound
,)
556 The subdirectory "tmdb" containing the modules tmdb_api.py (v0.1.3 or greater), tmdb_ui.py,
557 tmdb_exceptions.py must have been installed with the MythTV python bindings.
562 if tmdb_api
.__version
__ < '0.1.3':
563 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
__)
568 try: # Check if the installation is equiped to directly search IMDB for movies
570 except ImportError, e
:
571 sys
.stderr
.write("\n! Error: To search for movies movies the IMDbPy library must be installed."\
572 "Check your installation's repository or check the following link."\
573 "from (http://imdbpy.sourceforge.net/?page=download)\nError:(%s)\n" % e
)
577 if imdb
.__version
__ < "3.8":
578 sys
.stderr
.write("\n! Error: You version the IMDbPy library (%s) is too old. You must use version 3.8 of higher." % imdb
.__version
__)
579 sys
.stderr
.write("Check your installation's repository or check the following link."\
580 "from (http://imdbpy.sourceforge.net/?page=download)\n")
583 class VideoTypes( DBData
):
586 setwheredat
= 'self.intid,'
587 logmodule
= 'Python VideoType'
592 c
.execute("""SELECT * FROM videotypes""")
594 for row
in c
.fetchall():
595 types
.append(VideoTypes(db
=db
, raw
=row
))
599 return "<VideoTypes '%s'>" % self
.extension
601 return str(self
).encode('utf-8')
602 def __init__(self
, id=None, ext
=None, db
=None, raw
=None):
604 DBData
.__init
__(self
, db
=db
, raw
=raw
)
606 DBData
.__init
__(self
, data
=(id,), db
=db
)
607 elif ext
is not None:
608 self
.__dict
__['where'] = 'extension=%s'
609 self
.__dict
__['wheredat'] = 'self.extension,'
610 DBData
.__init
__(self
, data
=(ext
,), db
=db
)
613 def isValidPosixFilename(name
, NAME_MAX
=255):
614 """Checks for a valid POSIX filename
616 Filename: a name consisting of 1 to {NAME_MAX} bytes used to name a file.
617 The characters composing the name may be selected from the set of
618 all character values excluding the slash character and the null byte.
619 The filenames dot and dot-dot have special meaning.
620 A filename is sometimes referred to as a "pathname component".
622 name: (base)name of the file
623 NAME_MAX: is defined in limits.h (implementation-defined constants)
624 Maximum number of bytes in a filename
625 (not including terminating null).
626 Minimum Acceptable Value: {_POSIX_NAME_MAX}
627 _POSIX_NAME_MAX: Maximum number of bytes in a filename
628 (not including terminating null).
631 More information on http://www.opengroup.org/onlinepubs/009695399/toc.htm
633 return 1<=len(name
)<= NAME_MAX
and "/" not in name
and "\000" not in name
634 # end isValidPosixFilename()
637 # Two routines used for movie title search and matching
638 def is_punct_char(char
):
639 '''check if char is punctuation char
640 return True if char is punctuation
641 return False if char is not punctuation
643 return char
in string
.punctuation
645 def is_not_punct_char(char
):
646 '''check if char is not punctuation char
647 return True if char is not punctuation
648 return False if chaar is punctuation
650 return not is_punct_char(char
)
652 def _getExtention(URL
):
653 """Get the graphic file extension from a URL
654 return the file extention from the URL
656 (dirName
, fileName
) = os
.path
.split(URL
)
657 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
658 return fileExtension
[1:]
661 def _getFileList(dst
):
662 ''' Create an array of fully qualified file names
663 return an array of file names
669 for directory
in dst
:
671 directory
= unicode(directory
, 'utf8')
672 except (UnicodeEncodeError, TypeError):
674 for filename
in os
.listdir(directory
):
675 names
.append(os
.path
.join(directory
, filename
))
677 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
))
680 for video_file
in names
:
681 if os
.path
.isdir(video_file
):
682 new_files
= _getFileList([video_file
])
683 for new_file
in new_files
:
684 file_list
.append(new_file
)
686 file_list
.append(video_file
)
691 class singleinstance(object):
693 singleinstance - based on Windows version by Dragan Jovelic this is a Linux
694 version that accomplishes the same task: make sure that
695 only a single instance of an application is running.
698 def __init__(self
, pidPath
):
700 pidPath - full path/filename where pid for running application is to be
701 stored. Often this is ./var/<pgmname>.pid
706 # See if pidFile exists
708 if os
.path
.exists(pidPath
):
710 # Make sure it is not a "stale" pidFile
713 pid
=int(open(pidPath
, 'r').read().strip())
715 # Check list of running pids, if not running it is stale so
732 if not self
.lasterror
:
734 # Write my pid into pidFile to keep multiple copies of program from
737 fp
=open(pidPath
, 'w')
738 fp
.write(str(os
.getpid()))
741 def alreadyrunning(self
):
742 return self
.lasterror
745 if not self
.lasterror
:
747 os
.unlink(self
.pidPath
)
748 # end singleinstance()
752 graphicsDirectories
= {'banner': u'bannerdir', 'screenshot': u'episodeimagedir', 'coverfile': u'posterdir', 'fanart': u'fanartdir'}
753 dir_dict
={'posterdir': "VideoArtworkDir", 'bannerdir': 'mythvideo.bannerDir', 'fanartdir': 'mythvideo.fanartDir', 'episodeimagedir': 'mythvideo.screenshotDir', 'mythvideo': 'VideoStartupDir'}
754 storagegroupnames
= {u'Videos': u'mythvideo', u'Coverart': u'posterdir', u'Banners': u'bannerdir', u'Fanart': u'fanartdir', u'Screenshots': u'episodeimagedir'}
755 storagegroups
={u'mythvideo': [], u'posterdir': [], u'bannerdir': [], u'fanartdir': [], u'episodeimagedir': []}
# The gobal dictionary is only populated with the current hosts storage group entries
756 image_extensions
= ["png", "jpg", "bmp"]
758 def getStorageGroups():
759 '''Populate the storage group dictionary with the local host's storage groups.
762 records
= mythdb
.getStorageGroup(hostname
=localhostname
)
764 for record
in records
:
765 # Only include Video, coverfile, banner, fanart, screenshot and trailers storage groups
766 if record
.groupname
in storagegroupnames
.keys():
767 dirname
= record
.dirname
769 dirname
= unicode(record
.dirname
, 'utf8')
770 except (UnicodeDecodeError):
771 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']))
772 continue # Skip any line that has non-utf8 characters in it
773 except (UnicodeEncodeError, TypeError):
775 # Strip the trailing slash so it is consistent with all other directory paths in Jamu
776 if dirname
[-1:] == u
'/':
777 storagegroups
[storagegroupnames
[record
.groupname
]].append(dirname
[:-1])
779 storagegroups
[storagegroupnames
[record
.groupname
]].append(dirname
)
782 any_storage_group
= False
783 tmp_storagegroups
= dict(storagegroups
)
784 for key
in tmp_storagegroups
.keys():
785 if len(tmp_storagegroups
[key
]):
786 any_storage_group
= True
788 del storagegroups
[key
] # Remove empty SG directory arrays
789 if any_storage_group
:
790 # Verify that each storage group is an existing local directory
791 storagegroup_ok
= True
792 for key
in storagegroups
.keys():
793 for directory
in storagegroups
[key
]:
794 if not os
.access(directory
, os
.F_OK
):
795 sys
.stderr
.write(u
"\n! Error: The local Storage group (%s) directory (%s) does not exist\n" % (key
, directory
))
796 storagegroup_ok
= False
797 if not storagegroup_ok
:
799 # end getStorageGroups
803 """Takes a string, checks if it is numeric.
806 >>> _can_int("A test")
820 """Default non-interactive UI, which auto-selects first results
822 def __init__(self
, config
, log
):
826 def selectSeries(self
, allSeries
):
829 def selectMovieOrPerson(self
, allElements
):
830 return makeDict([allElements
[0]])
835 UI_search_language
= u
''
836 UI_selectedtitle
= u
''
837 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
838 # Hard-coded here as it is realtively static, and saves another HTTP request, as
839 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
840 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',}
842 class jamu_ConsoleUI(BaseUI
):
843 """Interactively allows the user to select a show or movie from a console based UI
846 def _displaySeries(self
, allSeries_array
):
847 """Helper function, lists series with corresponding ID
849 if video_type
== u
'IMDB':
850 URL
= u
'http://www.imdb.com/title/tt'
851 URL2
= u
'http://www.imdb.com/find?s=all&q='+urllib
.quote_plus(UI_title
.encode("utf-8"))+'&x=0&y=0'
853 elif video_type
== u
'TMDB':
854 URL
= u
'http://themoviedb.org/movie/'
855 URL2
= u
'http://themoviedb.org/'
858 URL
= u
'http://thetvdb.com/index.php?tab=series&id=%s&lid=%s'
859 URL2
= u
'http://thetvdb.com/?tab=advancedsearch'
864 for index
in range(len(allSeries_array
)):
865 allSeries
[allSeries_array
[index
]['name']] = allSeries_array
[index
]
866 tmp_names
= allSeries
.keys()
870 # Find any TV Shows or Movies who's titles start with the video's title
871 for name
in tmp_names
:
872 if filter(is_not_punct_char
, name
.lower()).startswith(filter(is_not_punct_char
, UI_title
.lower())):
873 most_likely
.append(name
)
875 # IMDB can return titles that are a movies foriegn title. The titles that do not match
876 # the requested title need to be added to the end of the most likely titles list.
877 if video_type
== u
'IMDB' and len(most_likely
):
878 for name
in tmp_names
:
880 dummy
= most_likely
.index(name
)
882 most_likely
.append(name
)
885 # Remove any name that does not start with a title like the TV Show/Movie (except for IMDB)
887 for likely
in most_likely
:
892 if not video_type
== u
'IMDB':
895 # reorder the list of series and sid's
897 for key
in names
: # list all search results
898 new_array
.append(allSeries
[key
])
900 # If there is only one to select and it is an exact match then return with no interface display
901 if len(new_array
) == 1:
902 if filter(is_not_punct_char
, allSeries_array
[0]['name'].lower()) == filter(is_not_punct_char
, UI_title
.lower()):
905 # Add the ability to select the skip inetref of '99999999'
906 new_array
.append( {'sid': '99999999', 'name': u'User choses to ignore video'}
)
907 names
.append(u
'User choses to ignore video')
910 for key
in names
: # list all search results
911 i_show
+=1 # Start at more human readable number 1 (not 0)
912 if key
== u
'User choses to ignore video':
913 print u
"% 2s -> %s # %s" % (
915 '99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
918 if video_type
!= u
'IMDB' and video_type
!= u
'TMDB':
919 tmp_URL
= URL
% (allSeries
[key
]['sid'], UI_langid_dict
[UI_search_language
])
920 print u
"% 2s -> %s # %s" % (
925 print u
"% 2s -> %s # %s%s" % (
928 allSeries
[key
]['sid']
930 print u
"Direct search of %s # %s" % (
936 def selectSeries(self
, allSeries
):
937 global UI_selectedtitle
938 UI_selectedtitle
= u
''
939 allSeries
= self
._displaySeries
(allSeries
)
941 # Check for an automatic choice
942 if len(allSeries
) <= 2:
943 for series
in allSeries
:
944 if filter(is_not_punct_char
, series
['name'].lower()) == filter(is_not_punct_char
, UI_title
.lower()):
945 UI_selectedtitle
= series
['name']
948 display_total
= len(allSeries
)
950 if video_type
== u
'IMDB':
954 elif video_type
== u
'TMDB':
959 reftype
= u
'Series id'
961 refformat
= u
"%6d" # Attempt to have the most likely TV/Movies at the top of the list
963 while True: # return breaks this loop
965 print u
'Enter choice ("Enter" key equals first selection (1)) or input the %s directly, ? for help):' % reftype
967 except KeyboardInterrupt:
968 raise tvdb_userabort(u
"User aborted (^c keyboard interupt)")
970 raise tvdb_userabort(u
"User aborted (EOF received)")
972 self
.log
.debug(u
'Got choice of: %s' % (ans
))
977 selected_id
= int(ans
) - 1 # The human entered 1 as first result, not zero
978 except ValueError: # Input was not number
980 self
.log
.debug(u
'Got quit command (q)')
981 raise tvdb_userabort(u
"User aborted ('q' quit command)")
984 print u
"# Enter the number that corresponds to the correct video."
985 print u
"# Enter the %s number for the %s." % (reftype
, video_type
)
986 print u
"# ? - this help"
989 self
.log
.debug(u
'Unknown keypress %s' % (ans
))
991 self
.log
.debug(u
'Trying to return ID: %d' % (selected_id
))
993 UI_selectedtitle
= allSeries
[selected_id
]['name']
994 return allSeries
[selected_id
]
996 if len(ans
) == refsize
and reftype
!= u
'Series id':
997 UI_selectedtitle
= u
''
998 return {'name': u'User input', 'sid': ans}
999 elif reftype
== u
'Series id':
1000 if len(ans
) >= refsize
:
1001 UI_selectedtitle
= u
''
1002 return {'name': u'User input', 'sid': ans}
1003 self
.log
.debug(u
'Invalid number entered!')
1004 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)
1005 UI_selectedtitle
= u
''
1006 self
._displaySeries
(allSeries
)
1008 #end while not valid_input
1010 def _useImageMagick(cmd
):
1011 """ Process graphics files using ImageMagick's utility 'mogrify'.
1012 >>> _useImageMagick('-resize 50% "poster.jpg"')
1016 return subprocess
.call(u
'mogrify %s > /dev/null' % cmd
, shell
=True)
1017 # end verifyImageMagick
1019 # Call a execute a command line process
1020 def callCommandLine(command
):
1021 '''Call a command line script or program. Display any errors
1022 return all stdoutput as a string
1024 p
= subprocess
.Popen(command
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
1027 data
= p
.stderr
.readline()
1029 sys
.stderr
.write(u
'%s\n' % data
)
1030 if data
== '' and p
.poll() != None:
1035 data
= p
.stdout
.readline()
1038 if data
== '' and p
.poll() != None:
1040 return returned_data
1041 # end callCommandLine
1044 # All functionality associated with configuration options
1045 class Configuration(object):
1046 """Set defaults, apply user configuration options, validate configuration settings and display the
1048 To view all available options run:
1049 >>> config = Configuration()
1050 >>> config.displayOptions()
1052 def __init__(self
, interactive
= False, debug
= False):
1053 """Initialize default configuration settings
1056 # Set all default variables
1057 self
.config
['interactive'] = interactive
1058 self
.config
['debug_enabled'] = debug
1059 self
.config
['flags_options'] = False
1060 self
.config
['local_language'] = u
'en'
1061 self
.config
['simulation'] = False
1062 self
.config
['toprated'] = False
1063 self
.config
['download'] = False
1064 self
.config
['nokeys'] = False
1065 self
.config
['maximum'] = None
1066 self
.config
['user_config'] = None
1067 self
.config
['overwrite'] = False
1068 self
.config
['update'] = False
1069 self
.config
['mythtvdir'] = False
1070 self
.config
['hd_dvd'] = ' HD - On DVD' # Used for HD DVD collection zero length video files
1071 self
.config
['dvd'] = ' - On DVD' # Used for DVD collection zero length video files
1073 self
.config
['video_dir'] = None
1074 self
.config
['recursive'] = True
1075 self
.config
['series_name'] = None
1076 self
.config
['sid'] = None
1077 self
.config
['season_num'] = None
1078 self
.config
['episode_num'] = None
1079 self
.config
['episode_name'] = None
1080 self
.config
['ret_filename'] = False
1082 # Flags for which data to perform actions on
1083 self
.config
['get_poster'] = False
1084 self
.config
['get_banner'] = False
1085 self
.config
['get_fanart'] = False
1086 self
.config
['get_ep_image'] = False
1087 self
.config
['get_ep_meta'] = False
1088 self
.config
['data_flags'] = ''
1089 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',]
1091 self
.config
['log'] = self
._initLogger
() # Setups the logger (self.log.debug() etc)
1093 # The default format of the file names (with and without episode names)
1094 self
.config
['with_ep_name'] = u
'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s'
1095 self
.config
['without_ep_name'] = u
'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s'
1096 self
.config
['ep_metadata'] = self
.config
['with_ep_name']
1098 # The default format of the graphics file names (with and without seasons and/or episode names)
1099 # The default is to use the URL's filename from thetvdb.com
1100 self
.config
['g_defaultname']=True
1101 # e.g. "Fringe - 01.jpg"
1102 self
.config
['g_series'] = u
'%(series)s - %(seq)s.%(ext)s'
1103 # e.g. "SG-1 - 07-02.jpg"
1104 self
.config
['g_season'] = u
'%(series)s - %(seasonnumber)02d-%(seq)s.%(ext)s'
1106 # Set default configuration variables
1107 # Start - Variables the user can override through option "-u" with their own file of variables
1108 self
.config
['allgraphicsdir'] = os
.getcwd()
1109 self
.config
['posterdir'] = None
1110 self
.config
['bannerdir'] = None
1111 self
.config
['fanartdir'] = None
1112 self
.config
['episodeimagedir'] = None
1113 self
.config
['metadatadir'] = None
1114 self
.config
['mythtvmeta'] = False
1115 self
.config
['myth_secondary_sources'] = {}
1116 self
.config
['posterresize'] = False
1117 self
.config
['fanartresize'] = False
1118 self
.config
['min_poster_size'] = 400
1119 self
.config
['image_library'] = False
1120 self
.config
['ffmpeg'] = True
1121 self
.config
['folderart'] = False
1122 self
.config
['metadata_exclude_as_update_trigger'] = ['intid', 'season', 'episode', 'showlevel', 'filename', 'coverfile', 'childid', 'browse', 'playcommand', 'trailer', 'host', 'screenshot', 'banner', 'fanart']
1123 self
.config
['filename_char_filter'] = u
"/%\000"
1124 self
.config
['ignore-directory'] = []
1127 # Dictionaries for Miro Bridge metadata downlods
1128 self
.config
['mb_tv_channels'] = {}
1129 self
.config
['mb_movies'] = {}
1131 # Episode data keys that you want to display or download.
1132 # This includes the order that you want them display or in the downloaded file.
1133 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']
1135 self
.config
['config_file'] = False
1136 self
.config
['series_name_override'] = False
1137 self
.config
['ep_name_massage'] = False
1138 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']
1141 # Regex pattern strings used to check for season number from directory names
1142 self
.config
['season_dir_pattern'] = [
1144 re
.compile(u
'''^.+?[ \._\-]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1146 re
.compile(u
'''^.+?[ \._\-][Ss]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1148 re
.compile(u
'''([0-9]+)[^\\/]*$''', re
.UNICODE
),
1150 re
.compile(u
'''[Ss]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1154 # Set default regex pattern strings used to extract series name , season and episode numbers for file name
1155 self
.config
['name_parse'] = [
1157 re
.compile(u
'''^(.+?)[ \._\-]\[[Ss]([0-9]+?)\]_\[[Ee]([0-9]+?)\]?[^\\/]*$''', re
.UNICODE
),
1159 re
.compile(u
'''^(.+?)[ \._\-]\[?([0-9]+)x([0-9]+)[^\\/]*$''', re
.UNICODE
),
1160 # foo.s01.e01, foo.s01_e01
1161 re
.compile(u
'''^(.+?)[ \._\-][Ss]([0-9]+)[\.\- ]?[Ee]([0-9]+)[^\\/]*$''' , re
.UNICODE
),
1163 re
.compile(u
'''^(.+)[ \._\-]([0-9]{1})([0-9]{2})[\._ -][^\\/]*$''' , re
.UNICODE
),
1165 re
.compile(u
'''^(.+)[ \._\-]([0-9]{2})([0-9]{2,3})[\._ -][^\\/]*$''' , re
.UNICODE
),
1168 # regex strings to parse folder names for TV series title, season and episode numbers
1169 self
.config
['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': u'Season', u'episode': u'Episode'}
1170 self
.config
['fullname_parse_regex'] = [
1171 # Title/Season 1/s01e01 Subtitle
1172 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1173 u
'''(?P<seasno>[0-9]+)/[Ss][0-9]+[Ee](?P<epno>[0-9]+).+$''',
1175 # Title/Season 1/1x01 Subtitle
1176 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1177 u
'''[0-9]+/(?P<seasno>[0-9]+)[Xx](?P<epno>[0-9]+).+$''',
1178 # Title/Season 1/01 Subtitle
1179 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1180 u
'''(?P<seasno>[0-9]+)/(?P<epno>[0-9]+).+$''',
1181 # Title/Season 1/Title s01e01 Subtitle
1182 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1183 u
'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s [Ss][0-9]+'''+
1184 u
'''[Ee](?P<epno>[0-9]+).+$''',
1185 # Title/Season 1/Title 1x01 Subtitle
1186 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1187 u
'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))'''+
1188 u
'''[Xx](?P<epno>[0-9]+).+$''',
1189 # Title/Season 1/Episode 1 Subtitle
1190 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1191 u
'''(?P<seasno>[0-9]+)/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1192 # Title/Season 1/Season 1 Episode 1 Subtitle
1193 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1194 u
'''(?P<seasno>[0-9]+)/%(season)s%(slash)s (?:(?P=seasno))%(slash)s '''+
1195 u
'''%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1196 # Title Season 1/01 Subtitle
1197 u
'''^.+?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1198 u
'''/(?P<epno>[0-9]+).+$''',
1199 # Title Season 1/s01e01 Subtitle
1200 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1201 u
'''/[Ss][0-9]+[Ee](?P<epno>[0-9]+).+''',
1202 # Title Season 1/1x01 Subtitle
1203 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1204 u
'''/(?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1205 # Title Season 1/Title s01e01 Subtitle
1206 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1207 u
'''/(?:(?P=seriesname))%(slash)s [Ss][0-9]+[Ee](?P<epno>[0-9]+).+$''',
1208 # Title Season 1/Title 1x01 Subtitle
1209 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1210 u
'''/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1211 # Title Season 1/Episode 1 Subtitle
1212 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1213 u
'''/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1214 # Title Season 1/Season 1 Episode 1 Subtitle
1215 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1216 u
'''/%(season)s%(slash)s (?:(?P=seasno))%(slash)s %(episode)s%(slash)s (?P<epno>[0-9]+).+$'''
1219 # Initalize a valriable used by the -MW option
1220 self
.program_seriesid
= None
1221 self
.config
[u
'file_move_flag'] = False
1226 data_flags_table
={ 'P': 'get_poster', 'B': 'get_banner', 'F': 'get_fanart', 'I': 'get_ep_image', 'E': 'get_ep_meta'}
1229 def _initLogger(self
):
1230 """Sets up a logger using the logging module, returns a log object
1232 logger
= logging
.getLogger(u
"jamu")
1233 formatter
= logging
.Formatter(u
'%(asctime)s) %(levelname)s %(message)s')
1235 hdlr
= logging
.StreamHandler(sys
.stdout
)
1237 hdlr
.setFormatter(formatter
)
1238 logger
.addHandler(hdlr
)
1240 if self
.config
['debug_enabled']:
1241 logger
.setLevel(logging
.DEBUG
)
1243 logger
.setLevel(logging
.WARNING
)
1247 def setUseroptions(self
, useroptions
):
1248 """ Change variables through a user supplied configuration file
1249 return False and exit the script if there are issues with the configuration file values
1251 if useroptions
[0]=='~':
1252 useroptions
=os
.path
.expanduser("~")+useroptions
[1:]
1253 if os
.path
.isfile(useroptions
) == False:
1255 "\n! Error: The specified user configuration file (%s) is not a file\n" % useroptions
1258 cfg
= ConfigParser
.SafeConfigParser()
1259 cfg
.read(useroptions
)
1260 for section
in cfg
.sections():
1261 if section
[:5] == 'File ':
1262 self
.config
['config_file'] = section
[5:]
1264 if section
== 'variables':
1265 # Change variables per user config file
1266 for option
in cfg
.options(section
):
1267 if option
== 'video_file_exts' or option
== 'tmdb_genre_filter' or option
== 'metadata_exclude_as_update_trigger':
1268 tmp_list
= (cfg
.get(section
, option
).rstrip()).split(',')
1269 for i
in range(len(tmp_list
)): tmp_list
[i
] = (tmp_list
[i
].strip()).lower()
1270 self
.config
[option
] = tmp_list
1272 if option
== 'filename_char_filter':
1273 for char
in cfg
.get(section
, option
):
1274 self
.config
['filename_char_filter']+=char
1276 if option
== 'translate':
1277 s_e
= (cfg
.get(section
, option
).rstrip()).split(',')
1278 if not len(s_e
) == 2:
1280 for index
in range(len(s_e
)):
1281 s_e
[index
] = s_e
[index
].strip()
1282 self
.config
['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': s_e[0], u'episode': s_e[1]}
1285 # Ignore user settings for Myth Video and graphics file directories
1286 # when the MythTV metadata option (-M) is selected
1287 if self
.config
['mythtvmeta'] and option
in ['posterdir', 'bannerdir', 'fanartdir', 'episodeimagedir', 'mythvideo']:
1289 self
.config
[option
] = cfg
.get(section
, option
)
1291 if section
== 'regex':
1292 # Change variables per user config file
1293 for option
in cfg
.options(section
):
1294 self
.config
['name_parse'].append(re
.compile(unicode(cfg
.get(section
, option
), 'utf8'), re
.UNICODE
))
1296 if section
== 'ignore-directory':
1297 # Video directories to be excluded from Jamu processing
1298 for option
in cfg
.options(section
):
1299 self
.config
['ignore-directory'].append(unicode(cfg
.get(section
, option
), 'utf8'))
1301 if section
=='series_name_override':
1303 for option
in cfg
.options(section
):
1304 overrides
[option
] = cfg
.get(section
, option
)
1305 if len(overrides
) > 0:
1306 self
.config
['series_name_override'] = overrides
1308 if section
=='ep_name_massage':
1310 for option
in cfg
.options(section
):
1311 tmp
=cfg
.get(section
, option
).split(',')
1312 if len(tmp
)%2 and len(cfg
.get(section
, option
)) != 0:
1313 sys
.stderr
.write(u
"\n! Error: For (%s) 'ep_name_massage' values must be in pairs\n" % option
)
1317 while i
!= len(tmp
):
1318 tmp
[i
] = tmp
[i
].strip()
1319 tmp
[i
+1] = tmp
[i
+1].strip()
1320 tmp_array
.append([tmp
[i
].replace('"',''), tmp
[i
+1].replace('"','')])
1322 massage
[option
]=tmp_array
1323 if len(massage
) > 0:
1324 self
.config
['ep_name_massage'] = massage
1326 if section
== 'ep_metadata_to_download':
1327 if len(cfg
.options(section
)):
1328 if cfg
.options(section
)[0] == 'ep_include_data':
1329 tmp
=cfg
.get(section
, cfg
.options(section
)[0])
1330 overrides
=tmp
.split(',')
1331 for index
in range(len(overrides
)):
1332 x
= overrides
[index
].replace(' ','')
1336 del overrides
[index
]
1337 self
.config
['ep_include_data']=overrides
1339 if section
== 'data_flags':
1340 if len(cfg
.options(section
)):
1341 for option
in cfg
.options(section
):
1342 if cfg
.get(section
, option
).lower() != 'False'.lower():
1343 for key
in self
.data_flags_table
.keys():
1344 if option
== self
.data_flags_table
[key
]:
1345 self
.config
[option
] = True
1347 for sec
in ['movies-secondary-sources', 'tv-secondary-sources']:
1350 for option
in cfg
.options(section
):
1351 secondary
[option
] = cfg
.get(section
, option
)
1352 if len(secondary
) > 0:
1353 self
.config
['myth_secondary_sources'][sec
[:sec
.index('-')]] = secondary
1355 if section
== u
'mb_tv':
1356 # Add the channel names and their corresponding thetvdb.com id numbers
1357 for option
in cfg
.options(section
):
1358 self
.config
['mb_tv_channels'][filter(is_not_punct_char
, option
.lower())] = [cfg
.get(section
, option
), u
'']
1360 if section
== u
'mb_movies':
1361 # Add the channel names for movie trailer Channels
1362 for option
in cfg
.options(section
):
1363 self
.config
['mb_movies'][filter(is_not_punct_char
, option
.lower())] = cfg
.get(section
, option
)
1366 # Expand any home directories that are not fully qualified
1367 dirs_to_check
= [u
'bannerdir', u
'episodeimagedir', u
'metadatadir', u
'posterdir', u
'video_dir', u
'fanartdir']
1368 for item
in dirs_to_check
:
1369 if self
.config
[item
]:
1370 if item
== u
'metadatadir' and not self
.config
[item
]:
1372 if self
.config
[item
][0]=='~':
1373 self
.config
[item
]=os
.path
.expanduser("~")+self
.config
[item
][1:]
1376 def displayOptions(self
):
1377 """ Display all of the configuration values. This is used to verify that the user has the
1378 variables set as they want before running jamu live.
1380 keys
=self
.config
.keys()
1383 ################### Used to create the example configuration file "jamu-example-conf"
1384 # for key in keys: # Used to create the example configuration file "jamu-example-conf"
1385 # print "#%s: %s" % (key, self.config[key])
1390 if key
== 'log': # Do not display the logger instance it is irrelevant for display
1393 if key
== 'name_parse':
1394 print u
"%s (%d items)" % (key
, len(self
.config
[key
]))
1396 print u
"%s (%s)" % (key
, str(self
.config
[key
]))
1399 print u
"%s (%d items)" % (key
, len(self
.config
[key
]))
1401 print u
"%s:" % key
, self
.config
[key
]
1402 # end set_Userconfig
1404 def changeVariable(self
, key
, value
):
1405 """Change any configuration variable - caution no validation is preformed
1407 self
.config
[key
]=value
1408 # end changeVariable
1411 def _checkNFS(self
, dirs
, ext_filter
):
1412 '''Check if any of the files are on NFS shares. If they are then the user must be warned.
1413 return True if there are at least one file is on a NFS share.
1414 return False if no graphic files are on an NFS share.
1417 for d
in dirs
: # Get rid of Null directories
1422 global localhostname
, graphicsDirectories
1424 localip
= gethostbyname(localhostname
) # Get the local hosts IP address
1425 except Exception, e
:
1426 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
))
1429 # Get all curently mounted NFS shares
1430 tmp_mounts
= callCommandLine("mount -l | grep '//'").split('\n')
1432 for mount
in tmp_mounts
:
1434 parts
= mount
.split(' ')
1435 tmparray
=[P
for P
in parts
]
1436 if tmparray
[0].startswith('//'): # Is this a NFS share definition
1437 if not tmparray
[0].startswith(u
'//%s' % localip
) and not tmparray
[0].startswith(u
'//%s' % localhostname
):
1438 nfs
.append(tmparray
[2]) # Add an NFS mount name
1440 if not len(nfs
): # Check if there are any NFS mounts
1443 # Check if any of the directories have files on an NFS share
1444 for directory
in dirs
: # Check the base directories first
1446 if os
.path
.realpath(directory
).startswith(mount
):
1448 for directory
in dirs
: # Check the actual files
1449 file_list
= _getFileList([directory
])
1450 if not len(file_list
):
1453 for fle
in file_list
: # Make a copy of file_list
1454 tmp_list
.append(fle
)
1455 for g_file
in tmp_list
: # Cull the list removing dirs and non-extention files
1456 if os
.path
.isdir(g_file
):
1457 file_list
.remove(g_file
)
1459 g_ext
= _getExtention(g_file
)
1460 if not g_ext
.lower() in ext_filter
:
1461 file_list
.remove(g_file
)
1463 for filename
in file_list
: # Actually check each file against the NFS mounts
1465 if os
.path
.realpath(filename
).startswith(mount
):
1471 def _getMythtvDirectories(self
):
1472 """Get all graphics directories found in the MythTV DB and change their corresponding
1473 configuration values. /media/video:/media/virtual/VB_Share/Review
1475 # Stop processing if this local host has any storage groups
1476 global localhostname
, storagegroups
1477 # Make sure Jamu is being run on a MythTV backend
1479 sys
.stderr
.write(u
"\n! Error: Jamu must be run on a MythTV backend. Local host (%s) is not a MythTV backend.\n" % localhostname
)
1483 for key
in dir_dict
.keys():
1484 graphics_dir
= mythdb
.settings
[localhostname
][dir_dict
[key
]]
1485 # Only use path from MythTV if one was found
1486 self
.config
[key
] = []
1487 if key
== 'mythvideo' and graphics_dir
:
1488 tmp_directories
= graphics_dir
.split(':')
1489 if len(tmp_directories
):
1490 for i
in range(len(tmp_directories
)):
1491 tmp_directories
[i
] = tmp_directories
[i
].strip()
1492 if tmp_directories
[i
] != '':
1493 if os
.access(tmp_directories
[i
], os
.F_OK
):
1494 self
.config
[key
].append(tmp_directories
[i
])
1497 sys
.stderr
.write(u
"\n! Warning: MythTV video directory (%s) does not exist.\n" % (tmp_directories
[i
]))
1500 if key
!= 'mythvideo' and graphics_dir
:
1501 if os
.path
.os
.access(graphics_dir
, os
.F_OK
):
1502 self
.config
[key
] = [graphics_dir
]
1504 sys
.stderr
.write(u
"\n! Warning: MythTV (%s) directory (%s) does not exist.\n" % (key
, graphics_dir
))
1506 # Save the FE path settings local to this backend
1507 self
.config
['localpaths'] = {}
1508 for key
in dir_dict
.keys():
1509 self
.config
['localpaths'][key
] = []
1511 if len(self
.config
[key
]):
1512 self
.config
['localpaths'][key
] = list(self
.config
[key
])
1514 # If there is a Videos SG then there is always a Graphics SG using Videos as a fallback
1516 for key
in dir_dict
.keys():
1517 if key
== 'episodeimagedir' or key
== 'mythvideo':
1519 if storagegroups
.has_key(u
'mythvideo') and not storagegroups
.has_key(key
):
1520 storagegroups
[key
] = list(storagegroups
[u
'mythvideo']) # Set fall back
1522 # Use Storage Groups as the priority but append any FE directory settings that
1523 # are local to this BE but are not already used as a storage group
1524 if storagegroups
.has_key(u
'mythvideo'):
1525 for key
in storagegroups
.keys():
1526 self
.config
[key
] = list(storagegroups
[key
])
1527 for k
in self
.config
['localpaths'][key
]:
1528 if not k
in self
.config
[key
]:
1529 self
.config
[key
].append(k
) # Add any FE settings local directories not already included
1531 if key
== 'mythvideo':
1532 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
))
1534 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
))
1537 # Make sure there is a directory set for Videos and other graphics directories on this host
1539 for key
in dir_dict
.keys():
1540 if key
== 'episodeimagedir': # Jamu does nothing with Screenshots
1542 # The fall back graphics SG is the Videos SG directory as of changeset 22104
1543 if storagegroups
.has_key(u
'mythvideo') and not len(self
.config
[key
]):
1544 self
.config
[key
] = storagegroups
[u
'mythvideo']
1545 if not len(self
.config
[key
]):
1546 sys
.stderr
.write(u
"\n! Error: There must be a directory for Videos and each graphic type. The (%s) directory is missing.\n" % (key
))
1551 # Make sure that the directory set for Videos and other graphics directories have the proper permissions
1553 for key
in dir_dict
.keys():
1554 for directory
in self
.config
[key
]:
1555 if key
== 'episodeimagedir': # Jamu does nothing with Screenshots
1557 if key
== 'mythvideo':
1558 if not os
.access(directory
, os
.F_OK | os
.R_OK
):
1559 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
, ))
1562 if not os
.access(directory
, os
.F_OK | os
.R_OK | os
.W_OK
):
1563 sys
.stderr
.write(u
"\n! Error: The (%s) directory (%s) must be read/writable for Jamu to function.\n" % (key
, directory
, ))
1568 # Print out the video and image directories that will be used for processing
1569 if self
.config
['mythtv_verbose']:
1570 dir_types
={'posterdir': "Cover art ", 'bannerdir': 'Banners ', 'fanartdir': 'Fan art ', 'episodeimagedir': 'Screenshots', 'mythvideo': 'Video '}
1571 sys
.stdout
.write(u
"\n==========================================================================================\n")
1572 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
)
1573 sys
.stdout
.write(u
"Note: All directories are from settings in the MythDB specific to hostname (%s).\n" % localhostname
)
1574 sys
.stdout
.write(u
"Note: Screenshot directories are not listed as Jamu does not process Screenshots.\n")
1575 sys
.stdout
.write(u
"------------------------------------------------------------------------------------------\n")
1576 for key
in dir_dict
.keys():
1577 if key
== 'episodeimagedir':
1579 for directory
in self
.config
[key
]:
1581 if storagegroups
.has_key(key
):
1582 if directory
in storagegroups
[key
]:
1584 sys
.stdout
.write(u
"Type: %s - SG-%s - Directory: (%s)\n" % (dir_types
[key
], sg_flag
, directory
))
1585 sys
.stdout
.write(u
"------------------------------------------------------------------------------------------\n")
1586 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")
1587 sys
.stdout
.write(u
"==========================================================================================\n\n")
1589 if self
.config
[u
'file_move_flag']: # verify the destination directory in a move is read/writable
1592 for arg
in self
.args
:
1596 if not os
.access(arg
, os
.F_OK
):
1597 for dirct
in self
.config
['mythvideo']:
1598 if arg
.startswith(dirct
):
1599 if not os
.access(dirct
, os
.F_OK | os
.R_OK | os
.W_OK
):
1600 sys
.stderr
.write(u
"! Error: Your move destination root MythVideo directory (%s) must be read/writable for Jamu to function.\n\n" % (dirct
, ))
1604 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
, ))
1606 elif not os
.access(arg
, os
.F_OK | os
.R_OK | os
.W_OK
):
1607 sys
.stderr
.write(u
"! Error: Your move destination directory (%s) must be read/writable for Jamu to function.\n\n" % (arg
, ))
1613 # Check if any Video files are on a NFS shares
1614 if not self
.config
['mythtvNFS']: # Maybe the NFS check is to be skipped
1615 if self
._checkNFS
(self
.config
['mythvideo'], self
.config
['video_file_exts']):
1616 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")
1618 # end _getMythtvDirectories
1621 def _JanitorConflicts(self
):
1622 '''Verify that there are no conflict between the graphics directories of MythVideo and
1623 other MythTV plugins. Write an warning message if a conflict is found.
1624 return True when there is a conflict
1625 return False when there is no conflict
1627 # Except for the plugins below no other plugins have non-theme graphics
1629 # Table 'settings' fields 'GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation'
1631 # Table 'settings' fields 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir'
1633 # Table 'settings' fields 'MusicLocation'
1634 global graphicsDirectories
, localhostname
1635 tablefields
= ['GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation', 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir', 'MusicLocation', 'ScreenShotPath']
1636 returnvalue
= False # Initalize as no conflicts
1637 for field
in tablefields
:
1638 tmp_setting
= mythdb
.settings
[localhostname
][field
]
1641 settings
= tmp_setting
.split(':') # Account for multiple dirs per setting
1642 if not len(settings
):
1644 for setting
in settings
:
1645 for directory
in graphicsDirectories
.keys():
1646 if not self
.config
[graphicsDirectories
[directory
]]:
1648 # As the Janitor processes subdirectories matching must be a starts with check
1649 for direc
in self
.config
[graphicsDirectories
[directory
]]:
1650 if os
.path
.realpath(setting
).startswith(os
.path
.realpath(direc
)):
1651 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
]]) )
1654 # end _JanitorConflicts
1657 def _addMythtvUserFileTypes(self
):
1658 """Add video file types to the jamu list from the "videotypes" table
1660 # Get videotypes table field names:
1662 records
= VideoTypes
.getAll()
1663 except MythError
, e
:
1664 sys
.stderr
.write(u
"\n! Error: Reading videotypes MythTV table: %s\n" % e
.args
[0])
1668 for record
in records
:
1669 # Remove any extentions that are in Jamu's list but the user wants ignore
1671 if record
.extension
in self
.config
['video_file_exts']:
1672 self
.config
['video_file_exts'].remove(record
.extension
)
1673 if record
.extension
.lower() in self
.config
['video_file_exts']:
1674 self
.config
['video_file_exts'].remove(record
.extension
.lower())
1675 else: # Add extentions that are not in the Jamu list
1676 if not record
.extension
in self
.config
['video_file_exts']:
1677 self
.config
['video_file_exts'].append(record
.extension
)
1678 # Make sure that all video file extensions are lower case
1679 for index
in range(len(self
.config
['video_file_exts'])):
1680 self
.config
['video_file_exts'][index
] = self
.config
['video_file_exts'][index
].lower()
1681 # end _addMythtvUserFileTypes()
1684 def validate_setVariables(self
, args
):
1685 """Validate the contents of specific configuration variables
1686 return False and exit the script if an invalid configuation value is found
1688 # Fix all variables which were changed by a users configuration files
1689 # to 'None', 'False' and 'True' literals back to their intended values
1690 keys
=self
.config
.keys()
1691 types
={'None': None, 'False': False, 'True': True}
1693 for literal
in types
.keys():
1694 if self
.config
[key
] == literal
:
1695 self
.config
[key
] = types
[literal
]
1697 # Compile regex strings to parse folder names for TV series title, season and episode numbers
1698 self
.config
['fullname_parse'] = []
1699 for index
in range(len(self
.config
['fullname_parse_regex'])):
1700 self
.config
['fullname_parse'].append(re
.compile(self
.config
['fullname_parse_regex'][index
] % self
.config
['fullname_parse_season_episode_translation'], re
.UNICODE
))
1702 if self
.config
['mythtvmeta']:
1703 if mythdb
== None or mythvideo
== None:
1704 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")
1708 self
.config
['image_library'] = Image
1709 except Exception, e
:
1710 sys
.stderr
.write(u
"""\n! Error: Python Imaging Library is required for figuring out the sizes of
1711 the fetched poster images.
1713 In Debian/Ubuntu it is packaged as 'python-imaging'.
1714 http://www.pythonware.com/products/pil/\nError:(%s)\n""" % e
)
1717 if not _can_int(self
.config
['min_poster_size']):
1718 sys
.stderr
.write(u
"\n! Error: The poster minimum value must be an integer (%s)\n" % self
.config
['min_poster_size'])
1721 self
.config
['min_poster_size'] = int(self
.config
['min_poster_size'])
1723 if self
.config
['maximum'] != None:
1724 if _can_int(self
.config
['maximum']) == False:
1725 sys
.stderr
.write(u
"\n! Error: Maximum option is not an integer (%s)\n" % self
.config
['maximum'])
1728 # Detect if this is a move request
1729 self
.config
[u
'file_move_flag'] = False
1731 if os
.path
.isfile(args
[0]) or os
.path
.isdir(args
[0]) or args
[0][-1:] == '*':
1732 self
.config
[u
'file_move_flag'] = True
1733 self
.args
= list(args
)
1735 if self
.config
['mythtvdir']:
1736 if mythdb
== None or mythvideo
== None:
1737 sys
.stderr
.write(u
"\n! Error: MythTV python interface is not available\n")
1739 if self
.config
['mythtvdir'] or self
.config
['mythtvmeta']:
1740 self
._addMythtvUserFileTypes
() # add user filetypes from the "videotypes" table
1741 self
._getMythtvDirectories
()
1742 if self
.config
['mythtvjanitor']: # Check for graphic directory conflicts with other plugins
1743 if self
._JanitorConflicts
():
1745 if not self
.config
['mythtvNFS']:
1746 global graphicsDirectories
, image_extensions
1748 for key
in graphicsDirectories
:
1749 if key
!= u
'screenshot':
1750 for directory
in self
.config
[graphicsDirectories
[key
]]:
1751 dirs
.append(directory
)
1752 # Check if any Graphics files are on NFS shares
1753 if self
._checkNFS
(dirs
, image_extensions
):
1754 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")
1757 if self
.config
['posterresize'] != False or self
.config
['fanartresize'] != False:
1758 if _useImageMagick("-version"):
1759 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'])))
1762 if self
.config
['mythtvmeta'] and len(args
) == 0:
1766 sys
.stderr
.write(u
"\n! Error: At least a video directory, SID or season name must be supplied\n")
1769 if os
.path
.isfile(args
[0]) or os
.path
.isdir(args
[0]) or args
[0][-1:] == '*':
1770 self
.config
['video_dir'] = []
1772 self
.config
['video_dir'].append(unicode(arg
,'utf8'))
1773 elif not self
.config
['mythtvmeta']:
1774 if _can_int(args
[0]) and len(args
[0]) >= 5:
1775 self
.config
['sid'] = unicode(args
[0], 'utf8') # There is still a chance that this is a series name "90210"
1777 if self
.config
['series_name_override']:
1778 if self
.config
['series_name_override'].has_key(args
[0].lower()):
1779 self
.config
['sid'] = unicode((self
.config
['series_name_override'][args
[0].lower()]).strip(), 'utf8')
1781 self
.config
['series_name'] = unicode(args
[0].strip(), 'utf8')
1783 self
.config
['series_name'] = unicode(args
[0].strip(), 'utf8')
1786 sys
.stderr
.write("\n! Error: Too many arguments (%d), maximum is three.\n" % len(args
))
1787 print "! args:", args
1789 if len(args
) == 3 and _can_int(args
[1]) and _can_int(args
[2]):
1790 self
.config
['season_num'] = args
[1]
1791 self
.config
['episode_num'] = args
[2]
1792 elif len(args
) == 3:
1793 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]))
1795 elif len(args
) == 2 and _can_int(args
[1]):
1796 self
.config
['season_num'] = args
[1]
1798 if self
.config
['ep_name_massage']:
1799 if self
.config
['ep_name_massage'].has_key(self
.config
['series_name']):
1800 tmp_ep_name
=args
[1].strip()
1801 tmp_array
=self
.config
['ep_name_massage'][self
.config
['series_name']]
1802 for pair
in tmp_array
:
1803 tmp_ep_name
= tmp_ep_name
.replace(pair
[0],pair
[1])
1804 self
.config
['episode_name'] = unicode(tmp_ep_name
, 'utf8')
1806 self
.config
['episode_name'] = unicode(args
[1].strip(), 'utf8')
1808 self
.config
['episode_name'] = unicode(args
[1].strip(), 'utf8')
1810 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
1811 # Hard-coded here as it is realtively static, and saves another HTTP request, as
1812 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
1813 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"]
1815 # Validate language as specified by user
1816 if self
.config
['local_language']:
1817 if not self
.config
['local_language'] in valid_languages
:
1819 for lang
in valid_languages
: valid_langs
+= lang
+', '
1820 valid_langs
=valid_langs
[:-2]
1821 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
))
1823 global UI_search_language
1824 UI_search_language
= self
.config
['local_language']
1826 if self
.config
['data_flags']:
1827 for data_type
in self
.config
['data_flags']:
1828 if self
.data_flags_table
.has_key(data_type
):
1829 self
.config
[self
.data_flags_table
[data_type
]]=True
1830 # end validate_setVariables
1833 """Return a copy of the configuration variables
1837 # end class Configuration
1840 class Tvdatabase(object):
1841 """Process direct thetvdb.com requests
1843 def __init__(self
, configuration
):
1844 """Retrieve all configuration options and get an instance of tvdb_api which is used to
1845 access thetvdb.com wiki.
1847 self
.config
= configuration
1848 cache_dir
=u
"/tmp/tvdb_api_%s/" % os
.geteuid()
1849 if self
.config
['interactive']:
1850 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)
1852 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)
1855 # High level dictionay keys for select graphics URL(s)
1856 fanart_key
=u
'fanart'
1857 banner_key
=u
'series'
1858 poster_key
=u
'poster'
1859 season_key
=u
'season'
1860 # Lower level dictionay keys for select graphics URL(s)
1861 poster_series_key
=u
'680x1000'
1862 poster_season_key
=u
'season'
1863 fanart_hires_key
=u
'1920x1080'
1864 fanart_lowres_key
=u
'1280x720'
1865 banner_series_key
=u
'graphical'
1866 banner_season_key
=u
'seasonwide'
1867 # Type of graphics being requested
1868 poster_type
=u
'poster'
1869 fanart_type
=u
'fanart'
1870 banner_type
=u
'banner'
1871 ep_image_type
=u
'filename'
1873 def sanitiseFileName(self
, name
):
1874 '''Take a file name and change it so that invalid or problematic characters are substituted with a "_"
1875 return a sanitised valid file name
1877 if name
== None or name
== u
'':
1879 for char
in self
.config
['filename_char_filter']:
1880 name
= name
.replace(char
, u
'_')
1882 name
= u
'_'+name
[1:]
1884 # end sanitiseFileName()
1887 def _getSeriesBySid(self
, sid
):
1888 """Lookup a series via it's sid
1889 return tvdb_api Show instance
1891 seriesid
= u
'sid:' + sid
1892 if not self
.corrections
.has_key(seriesid
):
1893 self
._getShowData
(sid
)
1894 self
.corrections
[seriesid
] = sid
1895 return self
.shows
[sid
]
1896 tvdb_api
.Tvdb
.series_by_sid
= _getSeriesBySid
1897 # end _getSeriesBySid
1899 def _searchforSeries(self
, sid_or_name
):
1900 """Get TV series data by sid or series name
1901 return None if the TV show was not found
1902 return an tvdb_api instance of the TV show data if it was found
1904 if self
.config
['sid']:
1905 show
= self
.config
['tvdb_api'].series_by_sid(self
.config
['sid'])
1907 self
.config
['series_name']=show
[u
'seriesname']
1910 if self
.config
['series_name_override']:
1911 if self
.config
['series_name_override'].has_key(sid_or_name
.lower()):
1912 self
.config
['sid'] = (self
.config
['series_name_override'][sid_or_name
.lower()])
1913 show
= self
.config
['tvdb_api'].series_by_sid(self
.config
['sid'])
1915 self
.config
['series_name'] = show
[u
'seriesname']
1918 show
= self
.config
['tvdb_api'][sid_or_name
]
1920 self
.config
['series_name'] = show
[u
'seriesname']
1923 show
= self
.config
['tvdb_api'][sid_or_name
]
1925 self
.config
['series_name'] = show
[u
'seriesname']
1927 # end _searchforSeries
1929 def verifySeriesExists(self
):
1932 Series and Season or
1933 Series and Season and Episode number or
1934 Series and Episode name
1935 passed by the user exists on thetvdb.com
1936 return False and display an appropriate error if the TV data was not found
1937 return an tvdb_api instance of the TV show/season/episode data if it was found
1939 sid
=self
.config
['sid']
1940 series_name
=self
.config
['series_name']
1941 season
=self
.config
['season_num']
1942 episode
=self
.config
['episode_num']
1943 episode_name
=self
.config
['episode_name']
1945 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
))
1946 if episode_name
: # Find an exact match for the series and episode name
1949 seriesfound
=self
._searchforSeries
(sid
).search(episode_name
)
1951 seriesfound
=self
._searchforSeries
(series_name
).search(episode_name
)
1952 if len(seriesfound
) != 0:
1953 for ep
in seriesfound
:
1954 if ep
['seriesid'] == '999999999':
1955 self
.config
['sid'] = ep
['seriesid']
1957 if (ep
['episodename'].lower()).startswith(episode_name
.lower()):
1958 if len(ep
['episodename']) > (len(episode_name
)+1):
1959 # Skip episodes the are not part of a set of (1), (2) ... etc
1960 if ep
['episodename'][len(episode_name
):len(episode_name
)+2] != ' (':
1962 series_sid
= ep
['seriesid']
1963 self
.config
['sid'] = ep
['seriesid']
1964 self
.config
['season_num'] = ep
['seasonnumber']
1965 self
.config
['episode_num'] = ep
['episodenumber']
1968 series_sid
= ep
['seriesid']
1969 self
.config
['sid'] = ep
['seriesid']
1970 self
.config
['season_num'] = ep
['seasonnumber']
1971 self
.config
['episode_num'] = ep
['episodenumber']
1973 raise tvdb_episodenotfound
1974 # Search for the series or series & season or series & season & episode
1976 if episode
: # series & season & episode
1977 seriesfound
=self
._searchforSeries
(series_name
)[int(season
)][int(episode
)]
1978 if seriesfound
['seriesid'] == '999999999':
1980 self
.config
['sid'] = seriesfound
['seriesid']
1981 self
.config
['episode_name'] = seriesfound
['episodename']
1982 else: # series & season
1983 seriesfound
=self
._searchforSeries
(series_name
)[int(season
)]
1985 seriesfound
=self
._searchforSeries
(series_name
) # Series only
1986 except tvdb_shownotfound
:
1987 # No such show found.
1988 # Use the show-name from the files name, and None as the ep name
1990 sys
.stderr
.write(u
"\n! Warning: Series (%s) not found\n" % (
1994 sys
.stderr
.write(u
"\n! Warning: Series TVDB number (%s) not found\n" % (
1998 except (tvdb_seasonnotfound
, tvdb_episodenotfound
, tvdb_attributenotfound
):
1999 # The season, episode or name wasn't found, but the show was.
2000 # Use the corrected show-name, but no episode name.
2001 if series_name
== None:
2004 sys
.stderr
.write(u
"\n! Warning: For Series (%s), season (%s) or Episode (%s) not found \n"
2005 % (series_name
, season
, episode
)
2008 sys
.stderr
.write(u
"\n! Warning: For Series (%s), Episode (%s) not found \n"
2009 % (series_name
, episode_name
)
2012 sys
.stderr
.write(u
"\n! Warning: For Series (%s), season (%s) not found \n" % (
2013 series_name
, season
)
2016 except tvdb_error
, errormsg
:
2017 # Error communicating with thetvdb.com
2018 if sid
: # Maybe the 5 digit number was a series name (e.g. 90210)
2019 self
.config
['series_name']=self
.config
['sid']
2020 self
.config
['sid'] = None
2021 return self
.verifySeriesExists()
2023 u
"\n! Warning: Error contacting www.thetvdb.com:\n%s\n" % (errormsg
)
2026 except tvdb_userabort
, errormsg
:
2027 # User aborted selection (q or ^c)
2028 print "\n", errormsg
2032 # end verifySeriesExists
2034 def _resizeGraphic(self
, filename
, resize
):
2035 """Resize a graphics file
2036 return False and display an error message if the graphics resizing failed
2037 return True if the resize was succcessful
2039 if self
.config
['simulation']:
2041 u
"Simulation resize command (mogrify -resize %s %s)\n" % (resize
, filename
)
2044 if _useImageMagick('-resize %s "%s"' % (resize
, filename
)):
2046 u
'\n! Warning: Resizing failed command (mogrify -resize %s "%s")\n' % (resize
, filename
)
2050 # end _resizeGraphic
2052 def _downloadURL(self
, url
, OutputFileName
):
2053 """Download the specified graphic file from a URL
2054 return False if no file was downloaded
2055 return True if a file was successfully downloaded
2057 # Only download a file if it does not exist or the option overwrite is selected
2058 if not self
.config
['overwrite'] and os
.path
.isfile(OutputFileName
):
2061 if self
.config
['simulation']:
2063 u
"Simulation download of URL(%s) to File(%s)\n" % (url
, OutputFileName
)
2068 tmp_URL
= url
.replace("http://", "")
2069 url
= "http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
2072 dat
= urllib
.urlopen(url
).read()
2074 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
))
2078 target_socket
= open(OutputFileName
, "wb")
2079 target_socket
.write(dat
)
2080 target_socket
.close()
2082 sys
.stderr
.write( u
"\n! Warning: Download IOError for Filename(%s), may be the directory is invalid\nError:(%s)\n" % (OutputFileName
, e
))
2085 # Verify that the downloaded file was NOT HTML instead of the intended file
2087 p
= subprocess
.Popen(u
'file "%s"' % OutputFileName
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
2088 except Exception, e
:
2089 sys
.stderr
.write( u
"\n! Warning: Download Exception for Filename(%s)\nError:(%s)\n" % (OutputFileName
, e
))
2093 data
= p
.stdout
.readline()
2095 data
= data
.encode('utf8')
2096 except UnicodeDecodeError:
2097 data
= unicode(data
,'utf8')
2098 index
= data
.find(u
'HTML document text')
2102 os
.remove(OutputFileName
) # Delete the useless HTML text
2103 if self
.config
['mythtv_verbose']:
2104 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
))
2108 def _setGraphicsFileNameFormat(self
):
2109 """Return a file name format (e.g. seriesname - episode name.extention)
2110 return a filename format string
2112 if self
.config
['g_defaultname']:
2113 return u
'%(url)s.%(ext)s'
2115 cfile
['seriesid']=self
.config
['sid']
2116 cfile
['series'] = self
.sanitiseFileName(self
.config
['series_name'])
2117 if cfile
['series'] != self
.config
['series_name']:
2118 self
.config
['g_series'] = self
.config
['g_series'].replace(self
.config
['series_name'], cfile
['series'])
2119 if self
.config
['season_num']:
2120 cfile
['seasonnumber']=int(self
.config
['season_num'])
2122 cfile
['seasonnumber']=0
2123 if self
.config
['episode_num']:
2124 cfile
['episodenumber']=int(self
.config
['episode_num'])
2126 cfile
['episodenumber']=0
2127 cfile
['episodename']=self
.config
['episode_name']
2128 cfile
['seq']=u
'%(seq)02d'
2129 cfile
['ext']=u
'%(ext)s'
2131 if self
.config
['season_num']:
2132 return self
.config
['g_season'] % cfile
2134 return self
.config
['g_series'] % cfile
2135 # end _setGraphicsFileNameFormat
2137 def _downloadGraphics(self
, urls
, mythtv
=False):
2138 """Download graphic file(s) from a URL list (string of one or more URLs separated by a CR
2140 return None is the string of urls has no urls
2141 return False if the any of the urls are corrupt
2142 return file name of the LAST file downloaded (special for MythTV data base updates)
2144 global graphicsDirectories
2146 if urls
== None: return None
2147 if urls
== '': return None
2148 tmp_list
=urls
.split('\n')
2154 if not len(url_list
):
2155 return None # There were no URLs in the list
2159 self
.config
['log'].debug(u
'Checking for a key in (%s)' % (x
))
2163 u
"\n! Warning: URL list does not have a graphics type key(%s)\n" % (x
)
2166 if url_dict
.has_key(x
[:i
]):
2167 temp_array
= [x
[i
+1:],'']
2168 url_dict
[x
[:i
]].append(temp_array
)# Collect a list of the same graphics type of URLs
2169 else: # The first URL of a new graphics type. Also URL replacement code left in place just in case
2170 url_dict
[x
[:i
]]=[[(x
[i
+1:]).replace(u
"http://www.thetvdb.com",u
"http://www.thetvdb.com"),'']]
2172 unique_dir
={u'poster': ['posterdir', True], u'banner': ['bannerdir', True], u'fanart': ['fanartdir', True], u'filename': ['episodeimagedir', True]}
2173 # If a graphics directory was not specified then default to the 'allgraphics' directory
2174 if not self
.config
['posterdir']: self
.config
['posterdir'] = self
.config
['allgraphicsdir']
2175 if not self
.config
['bannerdir']: self
.config
['bannerdir'] = self
.config
['allgraphicsdir']
2176 if not self
.config
['fanartdir']: self
.config
['fanartdir'] = self
.config
['allgraphicsdir']
2177 if not self
.config
['episodeimagedir']: self
.config
['episodeimagedir'] = self
.config
['allgraphicsdir']
2179 # Check if any of the downloaded graphics will share the same directory
2180 for key
in unique_dir
.keys():
2181 for k
in unique_dir
.keys():
2183 if self
.config
[unique_dir
[key
][0]] == self
.config
[unique_dir
[k
][0]]:
2184 unique_dir
[key
][1] = False
2187 dirs
={u
'poster': self
.config
['posterdir'], u
'banner': self
.config
['bannerdir'],
2188 u
'fanart': self
.config
['fanartdir'], u
'filename': self
.config
['episodeimagedir']}
2190 # Figure out filenaming convention
2191 file_format
= self
._setGraphicsFileNameFormat
()
2193 # Set the graphics fully qualified filenames matched to a URL
2194 for URLtype
in url_dict
:
2196 if self
.absolutepath
:
2197 if URLtype
== 'poster':
2198 tmpgraphicdir
= graphicsDirectories
['coverfile']
2200 tmpgraphicdir
= graphicsDirectories
[URLtype
]
2201 if not len(self
.config
['localpaths'][tmpgraphicdir
]):
2204 directory
= self
.config
['localpaths'][tmpgraphicdir
][0]
2206 directory
= dirs
[URLtype
][0]
2208 directory
= dirs
[URLtype
]
2210 for url
in url_dict
[URLtype
]:
2211 (dirName
, fileName
) = os
.path
.split(url
[0])
2212 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
2213 fileBaseName
= self
.sanitiseFileName(fileBaseName
)
2214 # Fix file extentions in all caps or 4 character JPEG extentions
2215 fileExtension
= fileExtension
.lower()
2216 if fileExtension
== '.jpeg':
2217 fileExtension
= '.jpg'
2218 cfile
={u'url': fileBaseName, u'seq': seq_num, u'ext': fileExtension[1:]}
2219 if not isValidPosixFilename(self
.config
['series_name']):
2220 if file_format
.startswith(self
.config
['series_name']):
2221 file_format
= file_format
.replace(self
.config
['series_name'], self
.sanitiseFileName(self
.config
['series_name']))
2222 cfile
['series'] = self
.sanitiseFileName(self
.config
['series_name'])
2223 cfile
['seriesid'] = self
.config
['sid']
2225 if URLtype
!= 'filename':
2226 if unique_dir
[URLtype
][1]:
2227 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+file_format
% cfile
2230 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+file_format
% cfile
2232 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+URLtype
.capitalize()+' - '+file_format
% cfile
2234 if self
.config
['season_num']:
2235 cfile
['seasonnumber']=int(self
.config
['season_num'])
2237 cfile
['seasonnumber'] = 0
2238 if self
.config
['episode_num']:
2239 cfile
['episodenumber']=int(self
.config
['episode_num'])
2241 cfile
['episodenumber'] = 0
2242 cfile
['episodename'] = self
.sanitiseFileName(self
.config
['episode_name'])
2243 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+self
.config
['ep_metadata'] % cfile
2246 # Download the graphics and resize if requested - Ignore download or resize issues!
2247 failed_download
= False
2248 for URLtype
in url_dict
:
2250 for pairs
in url_dict
[URLtype
]:
2251 if self
._downloadURL
(pairs
[0], pairs
[1]):
2252 if URLtype
== u
'poster' and self
.config
['posterresize']:
2253 self
._resizeGraphic
(pairs
[1], self
.config
['posterresize'])
2254 elif URLtype
== u
'fanart' and self
.config
['fanartresize']:
2255 self
._resizeGraphic
(pairs
[1], self
.config
['fanartresize'])
2256 elif not os
.path
.isfile(pairs
[1]): # Check if the file already was downloaded
2257 failed_download
= True # The download failed
2258 if self
.config
['mythtv_verbose']:
2259 sys
.stderr
.write(u
'\nA graphics file failed to be downloaded. A file issue or a corrupt (HTML) file.(%s)\n' % pairs
[1])
2261 if self
.config
['maximum']: # Has the maximum number of graphics been downloaded?
2262 if seq_num
== int(self
.config
['maximum']):
2267 return pairs
[1] # The name of the LAST graphics successfully downloaded
2268 # end _downloadGraphics
2270 def getGraphics(self
, graphics_type
):
2271 """Retrieve Poster or Fan Art or Banner or Episode image graphics URL(s)
2272 return None if no graphics URLs were found
2273 return a string of URLs
2276 series_name
=self
.config
['series_name']
2277 season
=self
.config
['season_num']
2278 episode
=self
.config
['episode_num']
2279 episode_name
=self
.config
['episode_name']
2280 lang
=self
.config
['local_language']
2284 if self
.config
['sid']:
2285 URLs
= self
.config
['tvdb_api'].ttvdb_parseBanners(self
.config
['sid'])
2287 URLs
= self
.config
['tvdb_api'].ttvdb_parseBanners(self
.config
['tvdb_api']._nameToSid
(series_name
))
2288 except Exception, e
:
2291 if graphics_type
== self
.fanart_type
: # Series fanart graphics
2292 if not len(URLs
[u
'fanart']):
2294 for url
in URLs
[u
'fanart']:
2295 graphics
.append(url
)
2296 elif season
== None and episode
== None and episode_name
== None:
2297 if not len(URLs
[u
'series']):
2299 if graphics_type
== self
.banner_type
: # Series Banners
2300 for url
in URLs
[u
'series']:
2301 graphics
.append(url
)
2302 else: # Series Posters
2303 for url
in URLs
[u
'poster']:
2304 graphics
.append(url
)
2306 if not len(URLs
[u
'season']):
2308 if graphics_type
== self
.banner_type
: # Season Banners
2310 for url
in URLs
[u
'season']:
2311 if url
[u
'bannertype2'] == u
'seasonwide' and url
[u
'season'] == season
:
2312 season_banners
.append(url
)
2313 if not len(season_banners
):
2315 graphics
= season_banners
2316 else: # Season Posters
2318 for url
in URLs
[u
'season']:
2319 if url
[u
'bannertype2'] == u
'season' and url
[u
'season'] == season
:
2320 season_posters
.append(url
)
2321 if not len(season_posters
):
2323 graphics
= season_posters
2326 if self
.config
['nokeys'] and not self
.config
['download']:
2329 key_tag
=graphics_type
+u
':'
2332 wasanythingadded
= 0
2333 anyotherlanguagegraphics
=u
''
2334 englishlanguagegraphics
=u
''
2335 for URL
in graphics
:
2336 if graphics_type
== 'filename':
2337 if URL
[graphics_type
] == None:
2339 if lang
: # Is there a language to filter URLs on?
2340 if lang
== URL
['language']:
2341 if graphics_type
!= self
.ep_image_type
:
2342 graphicsURLs
+=key_tag
+URL
['_bannerpath']+'\n'
2344 graphicsURLs
+=key_tag
+URL
[graphics_type
]+'\n'
2345 else: # Check for fall back graphics in case there are no selected language graphics
2346 if u
'en' == URL
['language']:
2347 if graphics_type
!= self
.ep_image_type
:
2348 englishlanguagegraphics
+=key_tag
+URL
['_bannerpath']+'\n'
2350 englishlanguagegraphics
+=key_tag
+URL
[graphics_type
]+'\n'
2352 if graphics_type
!= self
.ep_image_type
:
2353 anyotherlanguagegraphics
+=key_tag
+URL
['_bannerpath']+'\n'
2355 anyotherlanguagegraphics
+=key_tag
+URL
[graphics_type
]+'\n'
2357 if graphics_type
!= self
.ep_image_type
:
2358 graphicsURLs
+=key_tag
+URL
['_bannerpath']+'\n'
2360 graphicsURLs
+=key_tag
+URL
[graphics_type
]+'\n'
2361 if wasanythingadded
== len(graphicsURLs
):
2363 wasanythingadded
= len(graphicsURLs
)
2365 if self
.config
['maximum']: # Has the maximum number of graphics been downloaded?
2366 if count
== int(self
.config
['maximum']):
2369 if not len(graphicsURLs
):
2370 if len(englishlanguagegraphics
): # Fall back to English graphics
2371 graphicsURLs
= englishlanguagegraphics
2372 elif len(anyotherlanguagegraphics
): # Fall back-back to any available graphics
2373 graphicsURLs
= anyotherlanguagegraphics
2375 if self
.config
['debug_enabled']:
2376 print "\nGraphics:\n", graphicsURLs
2378 if not len(graphicsURLs
): # Are there any graphics?
2381 if len(graphicsURLs
) == 1 and graphicsURLs
[0] == graphics_type
+':':
2382 return None # Due to the language filter there may not be any URLs
2384 return(graphicsURLs
)
2387 def getTopRatedGraphics(self
, graphics_type
):
2388 """Retrieve only the top rated series Poster, Fan Art and Banner graphics URL(s)
2389 return None if no top rated graphics URLs were found
2390 return a string of top rated URLs
2392 if graphics_type
== u
'filename':
2393 self
.config
['log'].debug(u
'! There are no such thing as top rated Episode image URLs')
2396 series_name
=self
.config
['series_name']
2397 keys
=self
.config
['nokeys']
2398 if self
._searchforSeries
(series_name
)[graphics_type
] != None:
2399 if keys
and not self
.config
['download']:
2400 toprated
=(self
._searchforSeries
(series_name
)[graphics_type
])+'\n'
2402 toprated
=(u
'%s:%s\n' % (graphics_type
, self
._searchforSeries
(series_name
)[graphics_type
]))
2404 # end getTopRatedGraphics
2406 def _downloadEpisodeData(self
,ep_data
):
2407 """Down load episode meta data and episode image graphics
2408 return True whether or not there was episode data processed
2410 if not len(ep_data
):
2411 return True # There were no episode data in the list
2412 ep_data_list
=[] # An array of episode meta data
2414 first_key
=self
.config
['ep_include_data'][0]+':'
2415 key_size
=len(first_key
)
2417 while len(ep_data
): # Grab each episode's set of meta data
2419 self
.config
['log'].debug(u
'Parse out the episode data from an episode meta dats string')
2420 end
= ep_data
[key_size
:].index(first_key
)
2421 ep_data_list
.append(ep_data
[:end
+key_size
])
2422 ep_data
=ep_data
[end
+key_size
:]
2424 ep_data_list
.append(ep_data
)
2427 if not self
.config
['metadatadir']:
2428 self
.config
['metadatadir'] = os
.getcwd()
2430 # Process each episode's meta data
2431 for episode
in ep_data_list
:
2432 tmp_data
= episode
.split('\n')
2433 for i
in range(len(tmp_data
)):
2434 tmp_data
[i
] = tmp_data
[i
].rstrip()# Remove \n characters from the end of each record
2436 for data
in tmp_data
:
2438 self
.config
['log'].debug(u
'Checking for key in episode meta data')
2439 tmp_dict
[data
[:data
.index(':')]] = data
[data
.index(':')+1:]
2442 tmp_dict
['ext']='meta'
2444 for key
in ['seasonnumber', 'episodenumber']:
2445 if tmp_dict
.has_key(key
):
2446 tmp_dict
[key
] = int(tmp_dict
[key
])
2447 if not tmp_dict
.has_key(u
'episodename'):
2448 tmp_dict
[u
'episodename'] = u
''
2449 filename
="%s/%s" % (self
.config
['metadatadir'],self
.config
['ep_metadata'] % tmp_dict
)
2450 image_filename
= None
2451 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2452 url
= tmp_dict
['filename']
2454 if not self
.config
['episodeimagedir']:
2455 self
.config
['episodeimagedir'] = self
.config
['allgraphicsdir']
2456 (dirName
, fileName
) = os
.path
.split(url
)
2457 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
2458 tmp_dict
[u
'ext']=fileExtension
[1:]
2459 image_filename
= "%s/%s" % (self
.config
['episodeimagedir'], self
.config
['ep_metadata'] % tmp_dict
)
2460 # Only download a file if it does not exist or the option overwrite is selected
2461 # or the option update is selected and the local meta data file is
2462 # older than the episode data on thetvdb.com wiki
2464 if self
.config
['update'] and tmp_dict
.has_key('lastupdated') and os
.path
.isfile(filename
):
2465 if int(tmp_dict
['lastupdated']) > int(os
.path
.getmtime(filename
)):
2468 if not self
.config
['overwrite'] and not outofdate
:
2469 if self
.config
['get_ep_meta'] and self
.config
['get_ep_image']:
2471 if os
.path
.isfile(filename
) and os
.path
.isfile(image_filename
):
2474 if os
.path
.isfile(filename
):
2476 elif self
.config
['get_ep_meta']:
2477 if os
.path
.isfile(filename
):
2479 elif self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2480 url
= tmp_dict
['filename']
2482 if os
.path
.isfile(image_filename
):
2489 if self
.config
['simulation']:
2490 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2491 self
.config
['log'].debug(u
'Simulate downloading an episode image')
2492 url
= tmp_dict
['filename']
2494 sys
.stdout
.write(u
"Simulation create episode image file(%s)\n" % image_filename
)
2495 if self
.config
['get_ep_meta']:
2497 u
"Simulation create meta data file(%s)\n" % filename
2501 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2502 if tmp_dict
['filename'] != 'None':
2503 self
._downloadGraphics
('filename:'+tmp_dict
['filename'])
2505 # Write out an episode meta data file
2506 if self
.config
['get_ep_meta']:
2507 fHandle
= codecs
.open(filename
, 'w', 'utf8')
2508 fHandle
.write(episode
)
2512 # end _downloadEpisodeData
2514 def _changeToCommas(self
,meta_data
):
2515 """Remove '|' and replace with commas
2516 return the modified text
2518 if not meta_data
: return meta_data
2519 meta_data
= (u
'|'.join([d
for d
in meta_data
.split('| ') if d
]))
2520 return (u
', '.join([d
for d
in meta_data
.split(u
'|') if d
]))
2521 # end _changeToCommas
2523 def _changeAmp(self
, text
):
2524 """Change & values to ASCII equivalents
2525 return the modified text
2527 if not text
: return text
2528 text
= text
.replace(""", u
"'").replace("\r\n", u
" ")
2529 text
= text
.replace(r
"\'", u
"'")
2533 def getSeriesEpisodeData(self
):
2534 """Get Series Episode meta data. This can be one specific episode or all of a seasons episodes
2535 or all episodes for an entire series.
2536 return an empy sting of no episode meta data was found
2537 reurn a string containing key value pairs of episode meta data
2539 sid
=self
.config
['sid']
2540 series_name
=self
.config
['series_name']
2541 season_num
=self
.config
['season_num']
2542 episode_num
=self
.config
['episode_num']
2543 episode_name
=self
.config
['episode_name']
2549 tmp_cast
= self
._searchforSeries
(series_name
)[u
'_actors']
2554 for cast
in tmp_cast
:
2555 cast_members
+=(cast
['name']+u
', ').encode('utf8')
2556 if cast_members
!= '':
2558 cast_members
= cast_members
[:-2].encode('utf8')
2559 except UnicodeDecodeError:
2560 cast_members
= unicode(cast_members
[:-2],'utf8')
2561 cast_members
= self
._changeAmp
(cast_members
)
2562 cast_members
= self
._changeToCommas
(cast_members
)
2563 cast_members
=cast_members
.replace('\n',' ')
2568 genres_string
= self
._searchforSeries
(series_name
)[u
'genre'].encode('utf8')
2571 if genres_string
!= None and genres_string
!= '':
2572 genres
= self
._changeAmp
(genres_string
)
2573 genres
= self
._changeToCommas
(genres
)
2575 seasons
=self
._searchforSeries
(series_name
).keys() # Get the seasons for this series
2576 episodes_metadata
=u
''
2577 for season
in seasons
:
2578 if season_num
: # If a season was specified skip other seasons
2579 if season
!= int(season_num
):
2581 episodes
=self
._searchforSeries
(series_name
)[season
].keys()# Get the episodes for this season
2582 for episode
in episodes
: # If an episode was specified skip other episodes
2584 if episode
!= int(episode_num
):
2587 if sid
: # Ouput the full series name
2589 ep_data
["series"]=self
._searchforSeries
(sid
)[u
'seriesname'].encode('utf8')
2590 except AttributeError:
2594 ep_data
["series"]=self
._searchforSeries
(series_name
)[u
'seriesname'].encode('utf8')
2595 except AttributeError:
2597 available_keys
=self
._searchforSeries
(series_name
)[season
][episode
].keys()
2599 ep_data
[u
'gueststars']=''
2600 for key
in available_keys
:
2601 if self
._searchforSeries
(series_name
)[season
][episode
][key
] == None:
2604 text
= self
._searchforSeries
(series_name
)[season
][episode
][key
]
2605 text
= self
._changeAmp
(text
)
2606 text
= self
._changeToCommas
(text
)
2607 ep_data
[key
.lower()]=text
.replace('\n',' ')
2608 for key
in self
.config
['ep_include_data']: # Select and sort the required meta data
2609 if ep_data
.has_key(key
):
2610 if key
== u
'gueststars':
2611 if ep_data
[key
] == '':
2612 tmp
+=u
'Cast:%s\n' % cast_members
2614 if (len(ep_data
[key
]) > 128) and not ep_data
[key
].count(','):
2615 tmp
+=u
'Cast:%s\n' % cast_members
2617 tmp
+=u
'Cast:%s, %s\n' % (cast_members
, ep_data
[key
])
2620 tmp
+=u
'%s:%s\n' % (key
, ep_data
[key
])
2621 except UnicodeDecodeError:
2622 tmp
+=u
'%s:%s\n' % (key
, unicode(ep_data
[key
], "utf8"))
2623 tmp
+=u
'Runtime:%s\n' % self
._searchforSeries
(series_name
)[u
'runtime']
2625 tmp
+=u
'Genres:%s\n' % genres
2627 episodes_metadata
+=tmp
2628 return episodes_metadata
2629 # end Getseries_episode_data
2631 def returnFilename(self
):
2632 """Return a single file name (excluding file extension and directory), limited by the current
2633 variables (sid, season name, season number ... etc). Typically used when writing a meta file
2634 or naming/renaming a video file after a TV show recording.
2635 return False and out put an error if there not either a series id (SID) or series name
2636 return False and out put an error if there proper episode information (numbers or name)
2637 return False if the option (-MGF) used and there is not exact TV series name match
2638 return a specific episode filename
2640 sid
=self
.config
['sid']
2641 series_name
=self
.config
['series_name']
2642 season_num
=self
.config
['season_num']
2643 episode_num
=self
.config
['episode_num']
2644 episode_name
=self
.config
['episode_name']
2646 if not sid
and not series_name
:
2648 u
"\n! Warning: There must be at least series name or SID to request a filename\n"
2652 if season_num
and episode_num
:
2654 elif not episode_name
:
2656 u
'\n! Error: There must be at least "season and episode numbers" or "episode name" to request a filename\n'
2660 # Special logic must be used if the (-MG) guessing option has been requested
2661 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
2663 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
2664 except Exception, e
:
2665 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
))
2667 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
, self
.config
['series_name'].lower()):
2668 self
.config
['sid'] = allmatchingseries
['sid']
2669 self
.config
['series_name'] = allmatchingseries
['name']
2671 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'])
2674 episode
= self
.verifySeriesExists()
2676 if not episode
: # Make sure an episode was found
2678 u
'\n! Error: The episode was not found for series(%s), Episode name(%s)\n' % (series_name
, episode_name
)
2682 sid
=self
.config
['sid']
2684 if UI_selectedtitle
and (self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']):
2685 self
.config
['series_name'] = UI_selectedtitle
2687 series_name
=self
.config
['series_name']
2688 season_num
=self
.config
['season_num']
2689 episode_num
=self
.config
['episode_num']
2690 episode_name
=self
.config
['episode_name']
2692 tmp_dict
={'series': series_name, 'seasonnumber': season_num, 'episodenumber': episode_num, 'episodename': episode_name, 'sid': sid }
2695 for key
in ['seasonnumber', 'episodenumber']:
2696 if tmp_dict
.has_key(key
):
2697 tmp_dict
[key
] = int(tmp_dict
[key
])
2699 return self
.sanitiseFileName(u
"%s" % (self
.config
['ep_metadata'] % tmp_dict
)[:-1])
2700 # end returnFilename
2702 def processTVdatabaseRequests(self
):
2703 """Process the data/download requests as indicated by the variables
2704 return None if the series/season/episode does not exist
2705 return None if there is no data to process for the request actions
2706 return a string for display or further processing that satisfies the reqested actions
2708 if self
.verifySeriesExists():# Getting a filename is a single event nothing else is returned
2709 if self
.config
['ret_filename']:
2710 return self
.returnFilename()
2714 types
={'get_fanart': self.fanart_type, 'get_poster': self.poster_type, 'get_banner': self.banner_type}
2715 if self
.config
['toprated']:
2716 typegetGraphics
=self
.getTopRatedGraphics
2718 typegetGraphics
=self
.getGraphics
2720 if self
.verifySeriesExists():
2721 if self
.config
['download']: # Deal only with graphics display or downloads
2722 for key
in types
.keys():
2723 if key
== 'get_ep_image': # Ep image downloads processed below
2725 if self
.config
[key
]:
2726 if self
._downloadGraphics
(typegetGraphics(types
[key
])):
2728 u
"%s downloading successfully processed\n" % key
.title()
2732 for key
in types
.keys():
2733 if self
.config
[key
]:
2734 string
=typegetGraphics(types
[key
])
2737 if url_string
!= '':
2738 results
+=url_string
# Add graphic URLs to returned results
2740 # Should episode meta data or episode image be processed?
2741 if self
.config
['get_ep_meta'] or self
.config
['get_ep_image']:
2742 if self
.config
['download']: # Deal only with episode data display or download
2743 if self
._downloadEpisodeData
(self
.getSeriesEpisodeData()):
2745 u
"Episode meta data and/or images downloads successfully processed\n"
2748 eps_string
= self
.getSeriesEpisodeData()
2749 if eps_string
!= '':
2750 results
+=eps_string
# Add episode meta data to returned results
2755 if results
[len(results
)-1] == '\n':
2756 return results
[:len(results
)-1]
2761 # end processTVdatabaseRequests
2763 def __repr__(self
): # Just a place holder
2770 class VideoFiles(Tvdatabase
):
2771 """Process all video file and/or directories containing video files. These TV Series video
2772 files must be named so that a "series name or sid" and/or "season and episode number"
2773 can be extracted from the video file name. It is best to have renamed the TV series video files with
2774 tvnamer before using these files with jamu. Any video file without season and episode numbers is
2775 assumed to be a movie. Files that do not match the previously described criterion will be skipped.
2776 tvnamer can be found at:
2777 http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search
2779 def __init__(self
, configuration
):
2780 """Retrieve the configuration options
2782 super(VideoFiles
, self
).__init
__(configuration
)
2785 image_extensions
= ["png", "jpg", "bmp"]
2787 def _findFiles(self
, args
, recursive
= False, verbose
= False):
2789 Takes a file name or folder path and grabs files inside them. Does not recurse
2790 more than one level (if a folder is supplied, it will list files within),
2791 unless recurse is True, in which case it will recursively find all files.
2792 return an array of file names
2796 for cfile
in args
: # Directories must exist and be both readable and writable
2797 if os
.path
.isdir(cfile
) and not os
.access(cfile
, os
.F_OK | os
.R_OK
):
2798 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
))
2801 if os
.path
.isdir(cfile
):
2802 for directory
in self
.config
['ignore-directory']: # ignore directory list
2803 if not cfile
.startswith(directory
):
2806 if ignore
: # Skip this directory
2808 if os
.path
.isdir(cfile
):
2809 index
= cfile
.find(u
'VIDEO_TS')
2811 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
))
2814 cfile
= unicode(cfile
, u
'utf8')
2815 except (UnicodeEncodeError, TypeError):
2817 for sf
in os
.listdir(cfile
):
2819 newpath
= os
.path
.join(cfile
, sf
)
2821 sys
.stderr
.write(u
"\n! Error: This video file cannot be processed skipping:\n")
2822 sys
.stderr
.write(sf
)
2823 sys
.stderr
.write(u
"\nIt may be advisable to rename this file and try again.\n\n")
2825 if os
.path
.isfile(newpath
):
2826 allfiles
.append(newpath
)
2830 self
._findFiles
([newpath
], recursive
= recursive
, verbose
= verbose
)
2835 elif self
.config
[u
'file_move_flag'] and not os
.access(cfile
, os
.F_OK | os
.R_OK | os
.W_OK
):
2836 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
))
2837 elif os
.path
.isfile(cfile
) and os
.access(cfile
, os
.F_OK | os
.R_OK
):
2838 allfiles
.append(cfile
) # Files must exist and be at least readable
2845 def _processNames(self
, names
, verbose
=False, movies
=False):
2847 Takes list of names, runs them though the self.config['name_parse'] regex parsing strings
2848 to extract series name, season and episode numbers. Non-video files are skipped.
2849 return an array of dictionaries containing series name, season and episode numbers, file path and full filename and file extention.
2853 filepath
, filename
= os
.path
.split( f
)
2854 filename
, ext
= os
.path
.splitext( filename
)
2856 # Remove leading . from extension
2857 ext
= ext
.replace(u
".", u
"", 1)
2858 self
.config
['log'].debug(u
'Checking for a valid video filename extension')
2859 if not ext
.lower() in self
.config
[u
'video_file_exts']:
2860 for key
in self
.image_extensions
:
2864 sys
.stderr
.write(u
"\n! Warning: Skipping non-video file name: (%s)\n" % (f
))
2868 for r
in self
.config
['name_parse']:
2869 match
= r
.match(filename
)
2871 # If the filename does not match the default regular
2872 # expressions, try to match the file path + filename with the
2873 # extended fullpath regular expression so we can extract the
2874 # needed information out of the pathname
2876 for r
in self
.config
['fullname_parse']:
2877 match
= r
.match(os
.path
.join(filepath
, filename
))
2882 self
.config
['log'].debug(u
'matched reg:%s'%match
.re
.pattern
)
2883 seriesname
, seasno
, epno
= match
.groups()
2885 #remove ._- characters from name (- removed only if next to end of line)
2886 seriesname
= re
.sub("[\._]|\-(?=$)", " ", seriesname
).strip()
2887 # ramsi remove [en] tags
2888 seriesname
= re
.sub("(?:\[.*\])+", " ", seriesname
).strip()
2890 seasno
, epno
= int(seasno
), int(epno
)
2892 if self
.config
['series_name_override']:
2893 if self
.config
['series_name_override'].has_key(seriesname
.lower()):
2894 if len((self
.config
['series_name_override'][seriesname
.lower()]).strip()) == 7:
2895 categories
+=u
', Movie'
2897 if movie
.endswith(self
.config
['hd_dvd']):
2898 movie
= movie
.replace(self
.config
['hd_dvd'], '')
2899 categories
+=u
', DVD'
2902 if movie
.endswith(self
.config
['dvd']):
2903 movie
= movie
.replace(self
.config
['dvd'], '')
2904 categories
+=u
', DVD'
2905 movie
= re
.sub("[\._]|\-(?=$)", " ", movie
).strip()
2906 # ramsi remove [en] tags
2907 movie
= re
.sub("(?:\[.*\])+", " ", movie
).strip()
2909 allEps
.append({ 'file_seriesname':movie
,
2912 'filepath':filepath
,
2913 'filename':filename
,
2915 'categories': categories
2917 except UnicodeDecodeError:
2918 allEps
.append({ 'file_seriesname':unicode(movie
,'utf8'),
2921 'filepath':unicode(filepath
,'utf8'),
2922 'filename':unicode(filename
,'utf8'),
2923 'ext':unicode(ext
,'utf8'),
2924 'categories': categories
2927 categories
+=u
', TV Series'
2929 allEps
.append({ 'file_seriesname':seriesname
,
2932 'filepath':filepath
,
2933 'filename':filename
,
2935 'categories': categories
2937 except UnicodeDecodeError:
2938 allEps
.append({ 'file_seriesname':unicode(seriesname
,'utf8'),
2941 'filepath':unicode(filepath
,'utf8'),
2942 'filename':unicode(filename
,'utf8'),
2943 'ext':unicode(ext
,'utf8'),
2944 'categories': categories
2947 if movies
: # Account for " - On DVD" and " HD - On DVD" extra text on file names
2948 categories
+=u
', Movie'
2951 if movie
.endswith(self
.config
['hd_dvd']):
2952 movie
= movie
.replace(self
.config
['hd_dvd'], '')
2953 categories
+=u
', DVD'
2956 if movie
.endswith(self
.config
['dvd']):
2957 movie
= movie
.replace(self
.config
['dvd'], '')
2958 categories
+=u
', DVD'
2959 movie
= re
.sub("[\._]|\-(?=$)", " ", movie
).strip()
2960 # ramsi remove [en] tags
2961 movie
= re
.sub("(?:\[.*\])+", " ", movie
).strip()
2963 allEps
.append({ 'file_seriesname':movie
,
2966 'filepath':filepath
,
2967 'filename':filename
,
2969 'categories': categories
2971 except UnicodeDecodeError:
2972 allEps
.append({ 'file_seriesname':unicode(movie
,'utf8'),
2975 'filepath':unicode(filepath
,'utf8'),
2976 'filename':unicode(filename
,'utf8'),
2977 'ext':unicode(ext
,'utf8'),
2978 'categories': categories
2981 sys
.stderr
.write(u
"\n! Warning: Skipping invalid name: %s\n" % (f
))
2989 def processFileOrDirectory(self
):
2990 '''This routine is NOT used for MythTV meta data processing.
2991 If directory path has been specified then create a list of files that qualify as video
2992 files / including recursed directories.
2993 Then parse the list of file names to determine (series, season number, ep number and ep name).
2994 Skip any video file that cannot be parsed for sufficient info.
2995 Loop through the list:
2996 > Check if the series, season, ... exists, skip with debug message if none found
2997 > Set variable with proper info: sid, series, season and episode numbers
2998 > Process the file's information per the variable to get graphics and or meta data
2999 return False and an error message and exist the script if there are no video files to process
3000 return None when all processing was complete
3001 return a string of file names if the "Filename" process option was True
3004 allFiles
= self
._findFiles
(self
.config
['video_dir'], self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
3005 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'])
3007 if len(validFiles
) == 0:
3008 sys
.stderr
.write(u
"\n! Error: No valid video files found\n")
3011 path_flag
= self
.config
['metadatadir']
3012 for cfile
in validFiles
:
3013 sys
.stdout
.write(u
"# Processing %(file_seriesname)s (season: %(seasno)d, episode %(epno)d)\n" % (cfile
))
3014 self
.config
['sid']=None
3015 self
.config
['episode_name'] = None
3016 self
.config
['series_name']=cfile
['file_seriesname']
3017 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3018 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3019 if not path_flag
: # If no metaddata directory specified then default to the video file dir
3020 self
.config
['metadatadir'] = cfile
['filepath']
3021 if self
.verifySeriesExists():
3022 self
.config
['log'].debug(u
"Found series(%s) season(%s) episode(%s)" % (self
.config
['series_name'], self
.config
['season_num'], self
.config
['episode_num']))
3023 if self
.config
['ret_filename']:
3024 returned
= self
.processTVdatabaseRequests()
3025 if returned
!= None and returned
!= False:
3026 filenames
+=returned
+'\n'
3028 self
.processTVdatabaseRequests()
3030 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']))
3031 self
.config
['log'].debug("# Done")
3032 if len(filenames
) == 0:
3035 return filenames
[:-1] # drop the last '\n'
3036 # end processFileOrDirectory
3038 def __repr__(self
): # Just a place holder
3045 class MythTvMetaData(VideoFiles
):
3046 """Process all mythvideo video files, update the video files associated MythTV meta data.
3047 Download graphics for those video files from either thetvdb.com or themovie.com. Video file names
3048 for TV episodes must series name, season and episode numbers. The video file's movie name must be
3049 an exact match with a movie title in themoviedb.com or the MythTV database must have an entry for
3050 the video file with a TMDB or an IMDB number (db field 'intref').
3052 def __init__(self
, configuration
):
3053 """Retrieve the configuration options
3055 super(MythTvMetaData
, self
).__init
__(configuration
)
3059 # A dictionary of meta data keys and initialized values
3060 global graphicsDirectories
3061 movie_file_format
=u
"%s/%s.%s"
3062 graphic_suffix
= {u'coverfile': u'_coverart', u'fanart': u'_fanart', u'banner': u'_banner'}
3063 graphic_name_suffix
= u
"%s/%s%s.%s"
3064 graphic_name_season_suffix
= u
"%s/%s Season %d%s.%s"
3067 def _getSubtitle(self
, cfile
):
3068 '''Get the MythTV subtitle (episode name)
3070 return episode name string
3072 self
.config
['sid']=None
3073 self
.config
['episode_name'] = None
3074 self
.config
['series_name']=cfile
['file_seriesname']
3075 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3076 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3077 self
.verifySeriesExists()
3078 return self
.config
['episode_name']
3082 def hashFile(self
, name
):
3083 '''Create metadata hash values for mythvideo files
3085 return u'' if the was an error with the video file or the video file length was zero bytes
3087 filename
= self
.rtnRelativePath(name
, u
'mythvideo')
3088 # Use the MythVideo hashing protocol when the video is in a storage groups
3089 if filename
[0] != u
'/':
3090 hash_value
= FileOps(mythbeconn
.hostname
).getHash(filename
, u
'Videos')
3091 if hash_value
== u
'NULL':
3096 # Use a local hashing routine when video is not in a Videos storage group
3097 # For original code: http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#Python
3099 longlongformat
= 'q' # long long
3100 bytesize
= struct
.calcsize(longlongformat
)
3101 f
= open(name
, "rb")
3102 filesize
= os
.path
.getsize(name
)
3104 if filesize
< 65536 * 2: # Video file is too small
3106 for x
in range(65536/bytesize
):
3107 buffer = f
.read(bytesize
)
3108 (l_value
,)= struct
.unpack(longlongformat
, buffer)
3110 hash = hash & 0xFFFFFFFFFFFFFFFF #to remain as 64bit number
3111 f
.seek(max(0,filesize
-65536),0)
3112 for x
in range(65536/bytesize
):
3113 buffer = f
.read(bytesize
)
3114 (l_value
,)= struct
.unpack(longlongformat
, buffer)
3116 hash = hash & 0xFFFFFFFFFFFFFFFF
3118 returnedhash
= "%016x" % hash
3121 except(IOError): # Accessing to this video file caused and error
3125 def rtnRelativePath(self
, abpath
, filetype
):
3126 '''Check if there is a Storage Group for the file type (video, coverfile, banner, fanart, screenshot)
3127 and form an apprioriate relative path and file name.
3128 return a relative path and file name
3129 return an absolute path and file name if there is no storage group for the file type
3134 # There is a chance that this is already a relative path or there is no Storage group for file type
3135 if not len(storagegroups
):
3137 if not storagegroups
.has_key(filetype
) or abpath
[0] != '/':
3140 # The file must already be in one of the directories specified by the file type's storage group
3141 for directory
in storagegroups
[filetype
]:
3142 if abpath
.startswith(directory
):
3143 return abpath
[len(directory
)+1:]
3146 # end rtnRelativePath
3148 def rtnAbsolutePath(self
, relpath
, filetype
):
3149 '''Check if there is a Storage Group for the file type (mythvideo, coverfile, banner, fanart,
3150 screenshot) and form an appropriate absolute path and file name.
3151 return an absolute path and file name
3152 return the relpath sting if the file does not actually exist in the absolute path location
3154 if relpath
== None or relpath
== u
'':
3157 # There is a chance that this is already an absolute path
3158 if relpath
[0] == u
'/':
3161 if self
.absolutepath
:
3162 if not len(self
.config
['localpaths'][filetype
]):
3164 directories
= self
.config
['localpaths'][filetype
]
3166 directories
= self
.config
[filetype
]
3168 for directory
in directories
:
3169 abpath
= u
"%s/%s" % (directory
, relpath
)
3170 if os
.path
.isfile(abpath
): # The file must actually exist locally
3173 return relpath
# The relative path does not exist at all the metadata entry is useless
3174 # end rtnAbsolutePath
3177 def removeCommonWords(self
, title
):
3178 '''Remove common words from a title
3179 return title striped of common words
3183 wordList
= [u
'the ', u
'a ', u
' '] # common word list. Leave double space as the last value.
3184 title
= title
.lower()
3185 for word
in wordList
:
3186 title
= title
.replace(word
, u
'')
3189 return filter(is_not_punct_char
, title
.strip())
3190 # end removeCommonWords()
3193 def _getTmdbIMDB(self
, title
, watched
=False, IMDB
=False, rtnyear
=False):
3194 '''Find and exact match of the movie name with what's on themoviedb.com
3195 If IMDB is True return an imdb#
3196 If rtnyear is True return IMDB# and the movie year in a dictionary
3197 return False (no matching movie found)
3198 return imdb# and/or tmdb#
3200 global video_type
, UI_title
3201 UI_title
= title
.replace(self
.config
[u
'hd_dvd'], u
'')
3202 UI_title
= UI_title
.replace(self
.config
[u
'dvd'], u
'')
3204 if UI_title
[-1:] == ')': # Get rid of the (XXXX) year from the movie title
3205 tmp_title
= UI_title
[:-7].lower()
3207 tmp_title
= UI_title
.lower()
3209 if self
.config
['series_name_override']:
3210 if self
.config
['series_name_override'].has_key(tmp_title
):
3211 return (self
.config
['series_name_override'][tmp_title
]).strip()
3220 results
= [self
.config
['tmdb_api'].searchIMDB(IMDB
)]
3222 results
= self
.config
['tmdb_api'].searchTMDB(user_tmdb
)
3224 if results
.has_key('releasedate'):
3225 return {'name': "%s (%s)" % (results['title'], results['releasedate'][:4]), u'sid': results[u'inetref']}
3227 return {'name': "%s" % (results['title'], ), u'sid': results[u'inetref']}
3229 return results
['inetref']
3231 results
= self
.config
['tmdb_api'].searchTitle(tmp_title
)
3232 except TmdbMovieOrPersonNotFound
, e
:
3234 except Exception, errormsg
:
3235 self
._displayMessage
(u
"themoviedb.com error for Movie(%s) invalid data error (%s)" % (title
, errormsg
))
3238 self
._displayMessage
(u
"themoviedb.com error for Movie(%s)" % title
)
3241 # Check if user's interactive response (Skip, selection, input #)
3242 if len(results
[0]) and self
.config
['interactive']:
3243 if results
[0].has_key('userResponse'):
3244 # Check if the user selected a specific movie from the list
3245 if results
[0]['userResponse'] == 'User selected':
3247 if results
[0].has_key('released'):
3248 data
= {'name': "%s (%s)" % (results[0]['name'], results[0]['released'][:4]), u'sid': results[0][u'id']}
3250 data
= {'name': "%s" % (results[0]['name'], ), u'sid': results[0][u'id']}
3253 return results
[0]['id']
3254 # Check if the user has entered a TMDB number themselves
3255 if results
[0]['userResponse'] == 'User input':
3256 user_tmdb
= results
[0]['id']
3258 # Check if the user wants this video to be ignored by Jamu from now on
3259 if results
[0]['id'] == '99999999':
3263 return results
[0]['id']
3266 if IMDB
: # This is required to allow graphic file searching both by a TMDB and IMDB numbers
3268 if results
[0].has_key('imdb_id'):
3269 return results
[0]['imdb_id'][2:]
3275 if UI_title
[-1:] == ')':
3276 name
= UI_title
[:-7].lower() # Just the movie title
3277 year
= UI_title
[-5:-1] # The movie release year
3279 name
= tmp_title
.lower()
3281 name
= name
.strip().replace(' ', ' ')
3284 for movie
in results
:
3285 if self
.removeCommonWords(movie
['name']) == self
.removeCommonWords(name
):
3287 if movie
.has_key('released'):
3288 TMDB_movies
.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3290 TMDB_movies
.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}
)
3292 if movie
.has_key(u
'released'):
3293 if movie
['released'][:4] == year
:
3295 return {'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
3298 TMDB_movies
.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3301 TMDB_movies
.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}
)
3303 elif movie
.has_key('alternative_name'):
3304 if self
.removeCommonWords(movie
['alternative_name']) == self
.removeCommonWords(name
):
3306 if movie
.has_key('released'):
3307 TMDB_movies
.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3309 TMDB_movies
.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}
)
3311 if movie
.has_key(u
'released'):
3312 if movie
['released'][:4] == year
:
3314 return {'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
3317 TMDB_movies
.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3320 TMDB_movies
.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}
)
3323 # When there is only one match but NO year to confirm then it is OK to assume an exact match
3324 if len(TMDB_movies
) == 1 and year
== '':
3326 return TMDB_movies
[0]
3328 return TMDB_movies
[0][u
'sid']
3330 if imdb_lib
: # Can a imdb.com search be done?
3331 imdb_access
= imdb
.IMDb()
3334 movies_found
= imdb_access
.search_movie(tmp_title
.encode("ascii", 'ignore'))
3337 if not len(movies_found
):
3340 for movie
in movies_found
: # Get rid of duplicates
3341 try: # Protect against bad data from IMDBpy
3342 if movie
.has_key('year'):
3343 temp
= {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])}
3345 temp
= {imdb_access.get_imdbID(movie): movie['title']}
3348 if tmp_movies
.has_key(temp
.keys()[0]):
3350 tmp_movies
[temp
.keys()[0]] = temp
[temp
.keys()[0]]
3351 for movie
in tmp_movies
:
3352 if tmp_movies
[movie
][:-7].lower() == name
or self
.removeCommonWords(tmp_movies
[movie
][:-7]) == self
.removeCommonWords(name
):
3354 if tmp_movies
[movie
][-5:-1] == year
:
3356 return {'name': tmp_movies[movie], u'sid': movie}
3358 return u
"%07d" % int(movie
) # Pad out IMDB# with leading zeroes
3359 IMDB_movies
.append({'name': tmp_movies[movie], u'sid': movie}
)
3361 if len(IMDB_movies
) == 1: # If this is the only choice and titles matched then auto pick it
3362 if self
.removeCommonWords(IMDB_movies
[0]['name'][:-7]) == self
.removeCommonWords(name
):
3364 return IMDB_movies
[0]
3366 return u
"%07d" % int(IMDB_movies
[0][u
'sid'])
3368 # Does IMDB list this movie?
3369 if len(IMDB_movies
) == 0:
3372 # Did the user want an interactive interface?
3373 if not self
.config
['interactive']:
3376 # Force only an IMDB look up for a movie
3377 movies
= IMDB_movies
3380 ui
= jamu_ConsoleUI(config
= self
.config
, log
= self
.config
['log'])
3382 inetref
= ui
.selectSeries(movies
)
3383 except tvdb_userabort
:
3384 if video_type
==u
'IMDB' or len(IMDB_movies
) == 0:
3385 self
._displayMessage
(u
"1-No selection made for Movie(%s)" % title
)
3387 movies
= IMDB_movies
3390 inetref
= ui
.selectSeries(movies
)
3391 except tvdb_userabort
:
3392 self
._displayMessage
(u
"2-No selection made for Movie(%s)" % title
)
3395 if inetref
.has_key('sid'):
3396 if _can_int(inetref
['sid']):
3397 if inetref
['sid'] == '99999999':
3398 return inetref
['sid']
3400 if inetref
['name'] == u
'User input':
3402 data
= imdb_access
.get_movie(inetref
['sid'])
3403 if data
.has_key('long imdb title'):
3404 return {'name': data['long imdb title'], u'sid': inetref['sid']}
3405 elif data
.has_key('title'):
3406 return {'name': data['title'], u'sid': inetref['sid']}
3409 except imdb
._exceptions
.IMDbDataAccessError
:
3414 return u
"%07d" % int(inetref
['sid']) # Pad out IMDB# with leading zeroes
3421 def _getTmdbGraphics(self
, cfile
, graphic_type
, watched
=False):
3422 '''Download either a movie Poster or Fanart
3424 return full qualified path and filename of downloaded graphic
3426 if graphic_type
== u
'-P':
3427 graphic_name
= u
'poster'
3428 key_type
= u
'coverart'
3429 rel_type
= u
'coverfile'
3431 graphic_name
= u
'fanart'
3432 key_type
= u
'fanart'
3435 self
.config
['series_name']=cfile
['file_seriesname']
3437 if len(cfile
['inetref']) == 7: # IMDB number
3438 results
= self
.config
['tmdb_api'].searchIMDB(cfile
['inetref'])
3440 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3441 except TmdbMovieOrPersonNotFound
, e
:
3442 self
._displayMessage
(u
"0-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3444 except Exception, e
:
3445 self
._displayMessage
(u
"themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_name
, e
))
3449 if not results
.has_key(key_type
):
3450 self
._displayMessage
(u
"1-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3453 self
._displayMessage
(u
"1b-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3456 graphic_file
= (results
[key_type
].split(u
','))[0].strip() # Only want the first image URL
3458 self
.config
['g_defaultname']=False
3459 self
.config
['toprated'] = True
3460 self
.config
['nokeys'] = False
3462 self
.config
['sid']=None
3464 if self
.program_seriesid
== None:
3465 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season 1'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3467 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3469 self
.config
['g_series'] = cfile
['inetref']+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3470 if graphic_type
== '-P':
3475 self
.config
['season_num']= None # Needed to get graphics named in 'g_series' format
3477 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3479 tmp_URL
= graphic_file
.replace(u
"http://", u
"")
3480 graphic_file
= u
"http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
3481 value
= self
._downloadGraphics
(u
"%s:%s" % (g_type
, graphic_file
), mythtv
=True)
3483 self
.config
['overwrite'] = False # Turn off overwriting
3486 self
._displayMessage
(u
"2-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3489 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3490 # end _getTmdbGraphics
3492 def _getSecondarySourceGraphics(self
, cfile
, graphic_type
, watched
=False):
3493 '''Download from secondary source such as movieposter.com
3495 return full qualified path and filename of downloaded graphic
3497 if not len(self
.config
['myth_secondary_sources']):
3500 if graphic_type
== u
'coverfile':
3501 graphic_type
= u
'poster'
3502 rel_type
= u
'coverfile'
3504 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
3505 if not self
.config
['myth_secondary_sources'].has_key('movies'):
3507 if self
.config
['myth_secondary_sources']['movies'].has_key(graphic_type
):
3508 source
= self
.config
['myth_secondary_sources']['movies'][graphic_type
]
3509 if source
.find(u
'%(imdb)s') != -1:
3510 if len(cfile
['inetref']) != 7:
3512 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3513 except TmdbMovieOrPersonNotFound
, e
:
3514 self
._displayMessage
(u
"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_type
, e
))
3516 except Exception, e
:
3517 self
._displayMessage
(u
"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_type
, e
))
3521 if not results
.has_key('imdb'):
3522 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']))
3524 cfile
['imdb'] = results
['imdb']
3526 cfile
['imdb'] = cfile
['inetref']
3530 if not self
.config
['myth_secondary_sources'].has_key('tv'):
3532 if self
.config
['myth_secondary_sources']['tv'].has_key(graphic_type
):
3533 source
= self
.config
['myth_secondary_sources']['tv'][graphic_type
]
3537 self
.config
['series_name']=cfile
['file_seriesname']
3539 if self
.config
['simulation']:
3540 sys
.stdout
.write(u
"Simulating - downloading Secondary Source graphic (%s)\n" % cfile
['file_seriesname'])
3541 return u
"Simulated Secondary Source graphic filename place holder"
3543 # Test that the secondary's required data has been passed
3545 command
= source
% cfile
3547 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
))
3550 tmp_files
= callCommandLine(command
)
3552 self
._displayMessage
(u
"\n! Warning: Source (%s)\n could not find (%s) for (%s)(%s)\n" % (source
% cfile
, graphic_type
, cfile
['filename'], cfile
['inetref']))
3555 tmp_array
=tmp_files
.split('\n')
3556 if tmp_array
[0].startswith(u
'Failed'):
3557 self
._displayMessage
(u
"\n! Warning: Source (%s)\nfailed to download (%s) for (%s)(%s)\n" % (source
% cfile
, graphic_type
, cfile
['filename'], cfile
['inetref']))
3560 if tmp_array
[0].startswith(u
'file://'):
3561 tmp_files
=tmp_array
[0].replace(u
'file://', u
'')
3562 if not os
.path
.isfile(tmp_files
):
3563 sys
.stderr
.write(u
'\n! Error: The graphic file does not exist (%s)\n' % tmp_files
)
3566 # Fix file extentions in all caps or 4 character JPEG extentions
3567 fileExtension
= (_getExtention(tmp_files
)).lower()
3568 if fileExtension
== u
'jpeg':
3569 fileExtension
= u
'jpg'
3571 if self
.program_seriesid
== None:
3572 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], self
.sanitiseFileName(cfile
['file_seriesname']), self
.graphic_suffix
[rel_type
], fileExtension
)
3574 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], self
.sanitiseFileName(self
.program_seriesid
), self
.graphic_suffix
[rel_type
], fileExtension
)
3576 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], cfile
['inetref'], self
.graphic_suffix
[rel_type
], fileExtension
)
3578 if os
.path
.isfile(filename
): # This may be the same small file or worse then current
3580 (width
, height
) = self
.config
['image_library'].open(filename
).size
3581 (width2
, height2
) = self
.config
['image_library'].open(tmp_files
).size
3583 os
.remove(tmp_files
)
3588 # Verify that the downloaded file was NOT HTML instead of the intended file
3589 if self
._checkValidGraphicFile
(tmp_files
, graphicstype
=u
'', vidintid
=False) == False:
3590 os
.remove(tmp_files
) # Delete the useless HTML text
3592 shutil
.copy2(tmp_files
, filename
)
3593 os
.remove(tmp_files
)
3594 self
.num_secondary_source_graphics_downloaded
+=1
3595 return self
.rtnRelativePath(filename
, graphicsDirectories
[rel_type
])
3597 graphic_file
= tmp_array
[0]
3599 self
.config
['g_defaultname']=False
3600 self
.config
['toprated'] = True
3601 self
.config
['nokeys'] = False
3603 self
.config
['sid']=None
3605 if self
.program_seriesid
== None:
3606 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3608 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3610 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['inetref'])+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3611 g_type
= graphic_type
3613 self
.config
['season_num']= None # Needed to get graphics named in 'g_series' format
3615 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3617 tmp_URL
= graphic_file
.replace(u
"http://", u
"")
3618 graphic_file
= u
"http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
3619 value
= self
._downloadGraphics
(u
"%s:%s" % (g_type
, graphic_file
), mythtv
=True)
3621 self
.config
['overwrite'] = False # Turn off overwriting
3623 self
._displayMessage
(u
"Secondary source %s not found(%s)(%s)" % (graphic_file
, cfile
['filename'], cfile
['inetref']))
3626 self
.num_secondary_source_graphics_downloaded
+=1
3627 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3628 # end _getSecondarySourceGraphics
3630 def combineMetaData(self
, available_metadata
, meta_dict
, vid_type
=False):
3631 ''' Combine the current data with new meta data from primary or secondary sources
3632 return combinted meta data dictionary
3635 for key
in meta_dict
.keys():
3636 if key
in self
.config
['metadata_exclude_as_update_trigger']:
3639 if key
== 'inetref' and available_metadata
[key
] != meta_dict
[key
]:
3640 available_metadata
[key
] = meta_dict
[key
]
3642 if key
== 'releasedate' and available_metadata
[key
] != meta_dict
[key
]:
3643 available_metadata
[key
] = meta_dict
[key
]
3645 if key
== 'userrating' and available_metadata
[key
] == 0.0:
3646 available_metadata
[key
] = meta_dict
[key
]
3648 if key
== 'length' and available_metadata
[key
] == 0:
3649 available_metadata
[key
] = meta_dict
[key
]
3651 if key
== 'rating' and (available_metadata
[key
] == 'NR' or available_metadata
[key
] == 'Unknown'):
3652 available_metadata
[key
] = meta_dict
[key
]
3654 if key
== 'year' and available_metadata
[key
] == 1895:
3655 available_metadata
[key
] = meta_dict
[key
]
3657 if key
== 'category' and available_metadata
[key
] == 0:
3658 available_metadata
[key
] = meta_dict
[key
]
3660 if key
== 'inetref' and available_metadata
[key
] == '00000000':
3661 available_metadata
[key
] = meta_dict
[key
]
3664 available_metadata
[key
] = meta_dict
[key
]
3666 if vid_type
and key
== 'subtitle': # There are no subtitles in movies
3668 if key
== 'plot': # Remove any line-feeds from the plot. Mythvideo does not expect them.
3669 meta_dict
[key
] = meta_dict
[key
].replace('\n', ' ')
3670 if (vid_type
and key
== 'plot') and (meta_dict
[key
].find('@') != -1 or len(meta_dict
[key
].split(' ')) < 10):
3672 if vid_type
and key
== 'plot':
3673 if available_metadata
[key
] != None:
3674 if len(available_metadata
[key
].split(' ')) < 10 and len(meta_dict
[key
].split(' ')) > 10:
3675 available_metadata
[key
] = meta_dict
[key
]
3677 if not available_metadata
.has_key(key
): # Mainly for Genre, Cast and Countries
3678 available_metadata
[key
] = meta_dict
[key
]
3680 if available_metadata
[key
] == None or available_metadata
[key
] == '' or available_metadata
[key
] == 'None' or available_metadata
[key
] == 'Unknown':
3681 available_metadata
[key
] = meta_dict
[key
]
3683 return available_metadata
3684 # end combineMetaData
3687 def _getSecondarySourceMetadata(self
, cfile
, available_metadata
):
3688 '''Download meta data from secondary source
3689 return available_metadata (returns the current metadata unaltered)
3690 return dictionary of combined meta data
3692 if not len(self
.config
['myth_secondary_sources']):
3693 return available_metadata
3695 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
3696 if not self
.config
['myth_secondary_sources'].has_key('movies'):
3697 return available_metadata
3699 if self
.config
['myth_secondary_sources']['movies'].has_key('metadata'):
3700 source
= self
.config
['myth_secondary_sources']['movies']['metadata']
3701 if source
.find(u
'%(imdb)s') != -1:
3702 if len(cfile
['inetref']) != 7:
3704 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3705 except TmdbMovieOrPersonNotFound
, e
:
3706 self
._displayMessage
(u
"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile
['file_seriesname'], e
))
3707 return available_metadata
3708 except Exception, e
:
3709 self
._displayMessage
(u
"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile
['file_seriesname'], e
))
3710 return available_metadata
3712 return available_metadata
3713 if not results
.has_key('imdb'):
3714 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']))
3715 return available_metadata
3716 cfile
['imdb'] = results
['imdb']
3718 cfile
['imdb'] = cfile
['inetref']
3720 return available_metadata
3722 if not self
.config
['myth_secondary_sources'].has_key('tv'):
3723 return available_metadata
3725 if self
.config
['myth_secondary_sources']['tv'].has_key('metadata'):
3726 source
= self
.config
['myth_secondary_sources']['tv']['metadata']
3728 return available_metadata
3730 # Test that the secondary's required data has been passed
3732 command
= source
% cfile
3734 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
))
3735 return available_metadata
3737 self
.config
['series_name']=cfile
['file_seriesname']
3740 tmp_files
= (callCommandLine(command
)).decode("utf8")
3742 self
._displayMessage
(u
"1-Secondary source (%s)\ndid not find(%s)(%s) meta data dictionary cannot be returned" % (source
% cfile
, cfile
['filename'], cfile
['inetref']))
3743 return available_metadata
3746 tmp_array
=tmp_files
.split('\n')
3747 for element
in tmp_array
:
3748 element
= (element
.rstrip('\n')).strip()
3749 if element
== '' or element
== None:
3752 index
= element
.index(':')
3755 key
= element
[:index
].lower()
3756 data
= element
[index
+1:]
3757 if data
== None or data
== '':
3759 if key
== u
'inetref' and len(cfile
['inetref']) == 7:
3760 meta_dict
[key
] = cfile
['inetref']
3762 data
= self
._changeAmp
(data
)
3763 data
= self
._changeToCommas
(data
)
3766 meta_dict
[key
] = int(data
)
3770 if key
== 'userrating':
3772 meta_dict
[key
] = float(data
)
3776 if key
== 'runtime':
3778 meta_dict
['length'] = long(data
)
3782 if key
== 'movierating':
3783 meta_dict
['rating'] = data
3787 if len(data
.split(' ')) < 10: # Skip plots that are less than 10 words
3791 if key
== 'trailer':
3793 if key
== 'releasedate':
3795 meta_dict
[key
] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
3799 meta_dict
[key
] = data
3800 if not len(meta_dict
):
3801 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']))
3802 return available_metadata
3805 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=movie
)
3806 self
.num_secondary_source_metadata_downloaded
+=1
3807 return available_metadata
3808 # end _getSecondarySourceMetadata
3810 def _getTmdbMetadata(self
, cfile
, available_metadata
):
3811 '''Download a movie's meta data and massage the genres string
3812 return results for secondary sources when no primary source meta data
3813 return dictionary of metadata combined with data from a secondary source
3816 if len(cfile
['inetref']) == 7: # IMDB number
3817 meta_dict
= self
.config
['tmdb_api'].searchIMDB(cfile
['inetref'])
3819 meta_dict
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3820 except TmdbMovieOrPersonNotFound
, e
:
3821 self
._displayMessage
(u
"0-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3822 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3823 except Exception, e
:
3824 self
._displayMessage
(u
"themoviedb.com error for Movie(%s)(%s) meta data dictionary cannot be returned, error(%s)" % (cfile
['filename'], cfile
['inetref'], e
))
3825 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3827 if meta_dict
== None:
3828 self
._displayMessage
(u
"1-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3829 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3831 keys
= meta_dict
.keys()
3834 data
= meta_dict
[key
]
3837 if key
== 'homepage':
3839 data
= self
._changeAmp
(data
)
3840 data
= self
._changeToCommas
(data
)
3843 genre_array
= data
.split(',')
3844 for i
in range(len(genre_array
)):
3845 genre_array
[i
] = (genre_array
[i
].strip()).lower()
3846 if genre_array
[i
] in self
.config
['tmdb_genre_filter']:
3847 genres
+=genre_array
[i
].title()+','
3849 meta_dict
[key
] = u
''
3852 meta_dict
[key
] = genres
[:-1]
3853 if key
== 'trailer':
3857 meta_dict
[key
] = int(data
)
3861 if key
== 'userrating':
3863 meta_dict
[key
] = float(data
)
3868 meta_dict
['homepage'] = data
3870 if key
== 'releasedate':
3872 meta_dict
[key
] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
3876 if key
== 'runtime':
3878 meta_dict
['length'] = long(data
)
3882 if key
== 'movierating':
3883 meta_dict
['rating'] = data
3885 if meta_dict
.has_key('rating'):
3886 if meta_dict
['rating'] == '':
3887 meta_dict
['rating'] = 'Unknown'
3890 if available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None:
3891 filename
= u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
3892 meta_dict
['hash'] = self
.hashFile(filename
)
3893 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=True)
3894 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3896 self
._displayMessage
(u
"2-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3897 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3898 # end _getTmdbMetadata
3900 def _getTvdbGraphics(self
, cfile
, graphic_type
, toprated
=False, watched
=False):
3901 '''Download either a TV Series Poster, Banner, Fanart or Episode image
3903 return full qualified path and filename of downloaded graphic
3905 rel_type
= graphic_type
3906 if graphic_type
== u
'coverfile':
3907 graphic_type
= u
'poster'
3908 elif graphic_type
== u
'poster':
3909 rel_type
=u
'coverfile'
3911 self
.config
['g_defaultname']=False
3912 self
.config
['toprated'] = toprated
3913 self
.config
['nokeys'] = False
3914 self
.config
['maximum'] = u
'1'
3917 self
.config
['sid']=cfile
['inetref']
3919 self
.config
['sid']=None
3920 self
.config
['episode_name'] = None
3921 self
.config
['series_name']=cfile
['file_seriesname']
3923 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3924 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3926 # Special logic must be used if the (-MG) guessing option has been requested
3927 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
3929 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
3930 except Exception, e
:
3931 self
._displayMessage
(u
"tvdb Series not found(%s) or connection issues with thetvdb.com web site.\nError:(%s)\n" % (cfile
['filename'], e
))
3933 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
,cfile
['file_seriesname'].lower()):
3934 self
.config
['sid'] = allmatchingseries
['sid']
3935 self
.config
['series_name'] = allmatchingseries
['name']
3936 cfile
['file_seriesname'] = allmatchingseries
['name']
3938 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'])
3941 if not self
.verifySeriesExists():
3942 self
._displayMessage
(u
"tvdb Series not found(%s)" % cfile
['filename'])
3946 if self
.program_seriesid
== None:
3947 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season 1'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3948 self
.config
['g_season'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3950 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3951 self
.config
['g_season'] = self
.sanitiseFileName(self
.program_seriesid
)+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3953 # TV Series ALWAYS need the ' Season' in the file name incase the show name could clobber a Movie image
3954 # Season X is used so that a real season image is not overritten. It will be renamed later.
3955 self
.config
['g_series'] = self
.sanitiseFileName(self
.config
['series_name'])+u
' Season X'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3956 self
.config
['g_season'] = self
.sanitiseFileName(self
.config
['series_name'])+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3958 typegetGraphics
=self
.getTopRatedGraphics
3959 self
.config
['season_num']= None # Needed to get toprated graphics named in 'g_series' format
3961 typegetGraphics
=self
.getGraphics
3963 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3964 value
= self
._downloadGraphics
(typegetGraphics(graphic_type
), mythtv
=True)
3965 self
.config
['overwrite'] = False # Turn off overwriting
3969 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3970 # end _getTvdbGraphics
3972 def _getTvdbMetadata(self
, cfile
, available_metadata
):
3973 '''Download thetvdb.com meta data
3974 return what was input or results from a secondary source
3975 return dictionary of metadata
3977 global video_type
, UI_title
3978 video_type
=u
'TV series'
3979 UI_title
= cfile
['file_seriesname']
3982 self
.config
['nokeys'] = False
3983 self
.config
['sid']=None
3984 self
.config
['episode_name'] = None
3985 self
.config
['series_name']=cfile
['file_seriesname']
3986 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3987 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3988 if self
.config
['series_name_override']:
3989 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
3990 self
.config
['sid'] = (self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]).strip()
3992 # Special logic must be used if the (-MG) guessing option has been requested
3993 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
3995 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
3996 except Exception, e
:
3997 self
._displayMessage
(u
"tvdb Series not found(%s) or there are connection problems with thetvdb.com\nError(%s)" % (cfile
['filename'], e
))
3999 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
,cfile
['file_seriesname'].lower()):
4000 self
.config
['sid'] = allmatchingseries
['sid']
4001 self
.config
['series_name'] = allmatchingseries
['name']
4002 cfile
['file_seriesname'] = allmatchingseries
['name']
4004 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'])
4007 if not self
.verifySeriesExists():
4008 self
._displayMessage
(u
"tvdb Series not found(%s) meta data dictionary cannot be returned" % cfile
['filename'])
4009 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4011 if self
.config
['sid'] == '99999999':
4012 if not self
.config
['interactive']:
4013 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4015 return {'sid': self.config['sid'], 'title': cfile['file_seriesname']}
4018 tmp_array
=(self
.getSeriesEpisodeData()).split('\n')
4020 for element
in tmp_array
:
4021 element
= (element
.rstrip('\n')).strip()
4024 index
= element
.index(':')
4025 key
= element
[:index
].lower()
4026 data
= element
[index
+1:]
4030 meta_dict
['title'] = data
4032 if key
== 'seasonnumber':
4034 meta_dict
['season'] = int(data
)
4038 if key
== 'episodenumber':
4040 meta_dict
['episode'] = int(data
)
4044 if key
== 'episodename':
4045 meta_dict
['subtitle'] = data
4047 if key
== u
'overview':
4048 meta_dict
['plot'] = data
4050 if key
== u
'director' and data
== 'None':
4051 meta_dict
['director'] = ''
4053 if key
== u
'firstaired' and len(data
) > 4:
4055 meta_dict
['year'] = int(data
[:4])
4058 meta_dict
['firstaired'] = data
4060 meta_dict
['releasedate'] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
4066 meta_dict
['year'] = int(data
)
4070 if key
== 'seriesid':
4071 meta_dict
['inetref'] = data
4072 meta_dict
[key
] = data
4076 meta_dict
['userrating'] = float(data
)
4080 if key
== 'filename':# This "episodeimage URL clashed with the video file name and ep image
4081 continue # is not used yet. So skip fixes the db video filename from being wiped.
4082 if key
== 'runtime':
4084 meta_dict
['length'] = long(data
)
4088 meta_dict
[key
] = data
4091 if not meta_dict
.has_key('director'):
4092 meta_dict
['director'] = u
''
4093 meta_dict
['rating'] = u
'TV Show'
4094 # URL to TVDB web site episode web page for this series
4095 for url_data
in [u
'seriesid', u
'seasonid', u
'id']:
4096 if not url_data
in meta_dict
.keys():
4099 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'])
4100 if available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None:
4101 filename
= u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4102 meta_dict
['hash'] = self
.hashFile(filename
)
4103 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=False)
4104 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4106 self
._displayMessage
(u
"tvdb Series found (%s) but no meta data for dictionary" % cfile
['filename'])
4107 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4108 # end _getTvdbMetadata
4110 def _make_db_ready(self
, text
):
4111 '''Prepare text for inclusion into a DB
4113 return data base ready text
4115 if not text
: return text
4117 text
= text
.replace(u
'\u2013', u
"-")
4118 text
= text
.replace(u
'\u2014', u
"-")
4119 text
= text
.replace(u
'\u2018', u
"'")
4120 text
= text
.replace(u
'\u2019', u
"'")
4121 text
= text
.replace(u
'\u2026', u
"...")
4122 text
= text
.replace(u
'\u201c', u
'"')
4123 text
= text
.replace(u
'\u201d', u
'"')
4124 except UnicodeDecodeError:
4130 def _addCastGenreCountry(self
, data_string
, vim
, cast_genres_type
):
4131 '''From a comma delimited string of cast members, genres or countries add the ones
4132 not already in the myth db and update the video's meta data
4133 return True when successfull
4134 return False if failed
4136 if data_string
== '':
4138 data
= data_string
.split(',')
4139 for i
in range(len(data
)):
4140 data
[i
]=data
[i
].strip()
4146 if cast_genres_type
== 'genres':
4149 elif cast_genres_type
== 'cast':
4152 elif cast_genres_type
== 'countries':
4154 vim
.country
.add(item
)
4157 # end _addCastGenreCountry()
4163 def _moveDirectoryTree(self
, src
, dst
, symlinks
=False, ignore
=None):
4164 '''Move a directory tree from a given source to a given destination. Subdirectories will be
4165 created and synbolic links will be recreated in the new destination.
4166 return an array of two arrays. Names of files/directories moved and Errors found
4172 (src
, fileName
) = os
.path
.split(src
)
4174 names
= os
.listdir(unicode(src
, 'utf8'))
4175 except (UnicodeEncodeError, TypeError):
4176 names
= os
.listdir(src
)
4178 if os
.path
.isfile(src
):
4179 (src
, fileName
) = os
.path
.split(src
)
4183 names
= os
.listdir(unicode(src
, 'utf8'))
4184 except (UnicodeEncodeError, TypeError):
4185 names
= os
.listdir(src
)
4187 if ignore
is not None:
4188 ignored_names
= ignore(src
, names
)
4190 ignored_names
= set()
4193 if self
.config
['simulation']:
4194 sys
.stdout
.write(u
"Simulation creating subdirectories for file move (%s)\n" % dst
)
4196 self
._displayMessage
(u
"Creating subdirectories for file move (%s)\n" % dst
)
4197 os
.makedirs(dst
) # Some of the subdirectories may already exist
4202 if name
in ignored_names
:
4204 srcname
= os
.path
.join(src
, name
)
4205 dstname
= os
.path
.join(dst
, name
)
4207 if not os
.access(srcname
, os
.F_OK | os
.R_OK | os
.W_OK
): # Skip any file that is not RW able
4208 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
))
4211 if symlinks
and os
.path
.islink(srcname
):
4212 linkto
= os
.readlink(srcname
)
4213 if self
.config
['simulation']:
4214 sys
.stdout
.write(u
"Simulation recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto
, dstname
))
4216 os
.symlink(linkto
, dstname
)
4217 self
._displayMessage
(u
"Recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto
, dstname
))
4218 self
.num_symbolic_links
+=1
4219 elif os
.path
.isdir(srcname
):
4221 self
._displayMessage
(u
"Wildcard skipping subdirectory (%s)\n" % srcname
)
4223 self
.num_created_video_subdirectories
+=1
4224 self
._displayMessage
(u
"Move subdirectory (%s)\n" % srcname
)
4225 self
._moveDirectoryTree
(srcname
, dstname
, symlinks
, ignore
)
4227 if self
.config
['simulation']:
4229 if srcname
.startswith(org_src
[:-1]):
4230 sys
.stdout
.write(u
"Simulation move wild card file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4231 self
.num_moved_video_files
+=1
4232 self
.new_names
.append(dstname
)
4234 self
._displayMessage
(u
"Simulation of wildcard skipping file(%s)" % (srcname
,))
4236 sys
.stdout
.write(u
"Simulation move file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4237 self
.num_moved_video_files
+=1
4238 self
.new_names
.append(dstname
)
4241 if srcname
.startswith(org_src
[:-1]):
4242 self
._displayMessage
(u
"Move wild card 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
)
4247 self
._displayMessage
(u
"Wildcard skipping file(%s)" % (srcname
,))
4249 self
._displayMessage
(u
"Move file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4250 shutil
.move(srcname
, dstname
)
4251 self
.num_moved_video_files
+=1
4252 self
.new_names
.append(dstname
)
4253 # XXX What about devices, sockets etc.?
4254 except (IOError, os
.error
), why
:
4255 self
.errors
.append([srcname
, dstname
, str(why
)])
4256 # catch the Error from the recursive move tree so that we can
4257 # continue with other files
4259 self
.errors
.append([src
, dst
, u
"Unknown error"])
4261 return [self
.new_names
, self
.errors
]
4262 # end _moveDirectoryTree
4264 # local variable for move stats
4265 num_moved_video_files
=0
4266 num_created_video_subdirectories
=0
4267 num_symbolic_links
=0
4269 def _moveVideoFiles(self
, target_destination_array
):
4270 """Copy files or directories to a destination directory.
4271 If the -F filename option is set then rename TV series during the move process. The move will
4272 be interactive for identifying a movie's IMDB number or TV series if the -i option was also set.
4273 If there is a problem error message are displayed and the script exists. After processing
4274 print a statistics report.
4275 return a array of video file dictionaries to update in Mythvideo data base
4277 global UI_selectedtitle
4278 # Validate that the targets and destinations actually exist.
4280 for file_dir
in target_destination_array
:
4281 if os
.access(file_dir
, os
.F_OK | os
.R_OK
):
4283 # Destinations must all be directories
4284 if not os
.path
.isdir(file_dir
):
4285 sys
.stderr
.write(u
"\n! Error: Destinations must all be directories.\nThis destination is not a directory (%s)\n" % (file_dir
,))
4289 for directory
in self
.config
['mythvideo']:
4290 dummy_dir
= file_dir
.replace(directory
, u
'')
4291 if dummy_dir
!= tmp_dir
:
4294 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'], ))
4296 # Verify that a target file is really a video file.
4297 if file_dir
[-1:] != '*': # Skip wildcard file name targets
4298 if os
.access(file_dir
, os
.F_OK | os
.R_OK
): # Confirm that the file actually exists
4299 if not os
.path
.isdir(file_dir
):
4300 ext
= _getExtention(file_dir
)
4301 for tmp_ext
in self
.config
['video_file_exts']:
4302 if ext
.lower() == tmp_ext
:
4305 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'],))
4310 num_renamed_files
= 0
4311 num_mythdb_updates
= 0
4314 video_files_to_process
=[]
4316 while i
< len(target_destination_array
):
4317 src
= target_destination_array
[i
]
4319 if src
[-1:] == u
'*':
4322 (src
, fileName
) = os
.path
.split(src
)
4323 dst
= target_destination_array
[i
+1]
4327 results
= self
._moveDirectoryTree
(org_src
, dst
, symlinks
=False, ignore
=None)
4329 results
= self
._moveDirectoryTree
(src
, dst
, symlinks
=False, ignore
=None)
4330 if len(results
[1]): # Check if there are any errors
4331 sys
.stderr
.write(u
"\n! Warning: There were errors during moving, with these directories/files\n")
4332 for error
in results
[1]:
4333 sys
.stderr
.write(u
'\n! Warning: Source(%s), Destination(%s), Reason:(%s)\n' % (error
[0], error
[1], error
[2]))
4335 for name
in results
[0]:
4336 file_name
= os
.path
.join(dst
, name
)
4337 if os
.path
.isdir(file_name
):
4338 for dictionary
in self
._processNames
(_getFileList([file_name
]), verbose
= self
.config
['debug_enabled'], movies
=True):
4339 tmp_cfile_array
.append(dictionary
)
4341 for dictionary
in self
._processNames
([file_name
], verbose
= self
.config
['debug_enabled'], movies
=True):
4342 tmp_cfile_array
.append(dictionary
)
4344 # Is the source directory within a mythvideo directory? If it is,
4345 # update existing mythdb records else add the record as you already have the inetref
4346 for directory
in self
.config
['mythvideo']:
4347 if src
.startswith(directory
):
4348 for cfile
in tmp_cfile_array
:
4349 tmp_path
= src
+cfile
['filepath'].replace(dst
, u
'')
4350 video_file
= self
.rtnRelativePath(self
.movie_file_format
% (tmp_path
, cfile
['filename'], cfile
['ext']), 'mythvideo')
4351 tmp_filename
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), 'mythvideo')
4352 result
= mythvideo
.getVideo(exactfile
=video_file
)
4356 intid
= result
.intid
4358 result
= mythvideo
.getVideo(exactfile
=self
.movie_file_format
% (tmp_path
, cfile
['filename'], cfile
['ext']), host
=localhostname
.lower())
4362 intid
= result
.intid
4364 metadata
= Video(id=intid
, db
=mythvideo
)
4365 if tmp_filename
[0] == '/':
4367 self
.absolutepath
= True
4369 host
= localhostname
.lower()
4370 self
.absolutepath
= False
4372 if self
.config
['simulation']:
4373 sys
.stdout
.write(u
"Simulation Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file
, tmp_filename
))
4375 self
._displayMessage
(u
"Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file
, tmp_filename
))
4376 Video(id=intid
, db
=mythvideo
).update({'filename': tmp_filename, 'host': host}
)
4377 num_mythdb_updates
+=1
4381 cfile_array
.extend(tmp_cfile_array
)
4382 i
+=2 # Increment by 2 because array is int pairs of target and destination
4384 # Attempt to rename the video file
4385 if self
.config
['ret_filename']:
4386 for index
in range(len(cfile_array
)):
4387 cfile
= cfile_array
[index
]
4388 if self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']:
4389 sys
.stdout
.write(u
"\nAttempting to rename video filename (%s)\n" % cfile
['file_seriesname'])
4390 if cfile
['seasno'] == 0 and cfile
['epno'] == 0: # File rename for a movie
4393 if self
.config
['series_name_override']:
4394 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
4395 sid
= self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]
4397 data
= self
._getTmdbIMDB
(cfile
['file_seriesname'], rtnyear
=True)
4400 if data
[u
'sid'] == '99999999': # The user chose to ignore this video
4402 new_filename
= self
.sanitiseFileName(data
[u
'name'])
4406 imdb_access
= imdb
.IMDb()
4408 data
= imdb_access
.get_movie(sid
)
4409 if data
.has_key('long imdb title'):
4410 new_filename
= data
['long imdb title']
4411 elif data
.has_key('title'):
4412 new_filename
= self
.sanitiseFileName(namedata
['title'])
4415 except imdb
._exceptions
.IMDbDataAccessError
:
4418 if not sid
: # Cannot find this movie skip the renaming
4421 if not new_filename
:
4424 cfile_array
[index
]['file_seriesname'] = new_filename
4425 else: # File rename for a TV Series Episode
4426 UI_selectedtitle
= u
''
4428 self
.config
['sid'] = None
4429 self
.config
['series_name'] = cfile
['file_seriesname']
4430 if self
.config
['series_name_override']:
4431 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
4432 self
.config
['sid'] = self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]
4433 self
.config
['series_name'] = None
4434 self
.config
['season_num'] = u
"%d" % cfile
['seasno']
4435 self
.config
['episode_num'] = u
"%d" % cfile
['epno']
4436 self
.config
['episode_name'] = None
4437 new_filename
= self
.returnFilename()
4438 inetref
= self
.config
['sid']
4439 if inetref
== '99999999': # User chose to ignore this video
4443 if new_filename
== cfile
['filename']: # The file was already named to standard format
4444 self
._displayMessage
(u
"File is already the correct name(%s)\n" % cfile
['filename'])
4446 video_file
= self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4447 tmp_filename
= self
.movie_file_format
% (cfile
['filepath'], new_filename
, cfile
['ext'])
4448 if self
.config
['simulation']:
4449 sys
.stdout
.write(u
"Simulation file renamed from(%s) to(%s)\n" % (video_file
, tmp_filename
))
4451 if not os
.access(video_file
, os
.F_OK | os
.R_OK | os
.W_OK
):
4452 sys
.stdout
.write(u
"Cannot rename this file as it does not have read/write permissions set (%s)\n" % video_file
)
4454 self
._displayMessage
(u
"File renamed from(%s) to(%s)\n" % (video_file
, tmp_filename
))
4455 os
.rename(video_file
, tmp_filename
)
4456 num_renamed_files
+=1
4457 video_file
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), 'mythvideo')
4458 tmp_filename
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], new_filename
, cfile
['ext']), 'mythvideo')
4459 result
= mythvideo
.getVideo(exactfile
=video_file
)
4463 intid
= result
.intid
4465 result
= mythvideo
.getVideo(exactfile
=self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), host
=localhostname
.lower())
4469 intid
= result
.intid
4470 if tmp_filename
[0] == '/':
4472 self
.absolutepath
= True
4474 host
= localhostname
.lower()
4475 self
.absolutepath
= False
4477 metadata
= Video(id=intid
, db
=mythvideo
)
4478 if self
.config
['simulation']:
4479 sys
.stdout
.write(u
"Simulation Mythdb update for renamed file(%s)\n" % (tmp_filename
))
4481 self
._displayMessage
(u
"Mythdb update for renamed file(%s)\n" % (tmp_filename
))
4482 Video(id=intid
, db
=mythvideo
).update({'filename': tmp_filename, 'host': host}
)
4484 if self
.config
['simulation']:
4485 sys
.stdout
.write(u
"Simulation Mythdb add for renamed file(%s)\n" % (tmp_filename
))
4487 self
._displayMessage
(u
"Adding Mythdb record for file(%s)\n" % (tmp_filename
))
4489 initrec
[u
'title'] = cfile
['file_seriesname']
4490 initrec
[u
'filename'] = tmp_filename
4491 initrec
[u
'host'] = host
4492 initrec
[u
'inetref'] = inetref
4493 Video(db
=mythvideo
).create(initrec
)
4494 cfile_array
[index
]['filename'] = new_filename
4496 if self
.config
['simulation']:
4497 sys
.stdout
.write(u
'\n---------Simulated Statistics---------------')
4498 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
))
4501 # end _moveVideoFiles
4503 def _displayMessage(self
, message
):
4504 """Displays messages through stdout. Usually used with MythTv metadata updates in -V
4508 if message
[-1:] != '\n':
4510 if self
.config
['mythtv_verbose']:
4511 sys
.stdout
.write(message
)
4512 # end _displayMessage
4514 def _findMissingInetref(self
):
4515 '''Find any video file without a Mythdb record or without an inetref number
4516 return None if there are no new video files
4517 return a array of dictionary information on each video file that qualifies for processing
4519 directories
=self
.config
['mythvideo']
4521 if not len(directories
):
4522 sys
.stderr
.write(u
"\n! Error: There must be a video directory specified in MythTv\n")
4525 allFiles
= self
._findFiles
(directories
, self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
4526 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'], movies
=True)
4527 if len(validFiles
) == 0: # Is there video files to process?
4531 for cfile
in validFiles
:
4533 videopath
= self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4534 except UnicodeDecodeError:
4535 videopath
= os
.path
.join(unicode(cfile
['filepath'],'utf8'), unicode(cfile
['filename'],'utf8')+u
'.'+cfile
['ext'])
4537 # Find the MythTV meta data
4538 result
= mythvideo
.getVideo(exactfile
=videopath
)
4542 intid
= result
.intid
4544 result
= mythvideo
.getVideo(exactfile
=self
.rtnRelativePath(videopath
, 'mythvideo'), host
=localhostname
.lower())
4548 intid
= result
.intid
4550 missing_list
.append(cfile
)
4552 meta_dict
= Video(id=intid
, db
=mythvideo
)
4553 if self
.config
['video_dir']:
4554 if not mythvideo
.getVideo(exactfile
=meta_dict
[u
'filename'], host
=meta_dict
[u
'host']):
4555 missing_list
.append(cfile
)
4557 # There must be an Internet reference number. Get one for new records.
4558 if _can_int(meta_dict
['inetref']) and not meta_dict
['inetref'] == u
'00000000' and not meta_dict
['inetref'] == '':
4560 missing_list
.append(cfile
)
4563 # end _findMissingInetref
4565 def _checkValidGraphicFile(self
, filename
, graphicstype
=u
'', vidintid
=False):
4566 '''Verify that a graphics file is not really an HTML file
4567 return True if it is a graphics file
4568 return False if it is an HTML file
4570 # Verify that the graphics file is NOT HTML instead of the intended graphics file
4572 p
= subprocess
.Popen(u
'file "%s"' % filename
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
4574 # There is something wrong with the file but do NOT say it is invalid just in case!
4576 data
= p
.stdout
.readline()
4578 data
= data
.encode('utf8')
4579 except UnicodeDecodeError:
4580 data
= unicode(data
,'utf8')
4581 index
= data
.find(u
'HTML document text')
4584 elif self
.config
['simulation']:
4586 u
"Simulation deleting bad graphics file (%s) as it is really HTML\n" % (filename
, )
4590 u
"and the MythVideo record was corrected for the graphic reference.\n"
4594 os
.remove(filename
) # Delete the useless HTML text
4595 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
))
4598 if graphicstype
== u
'coverfile':
4599 repair
[graphicstype
] = u
'No Cover'
4601 repair
[graphicstype
] = u
''
4602 Video(id=vidintid
, db
=mythvideo
).update(repair
)
4604 # end _checkValidGraphicFile()
4607 def _graphicsCleanup(self
):
4608 '''Match the graphics in the mythtv graphics directories with the ones specified by the
4609 mythvideometa records. Remove any graphics that are not referenced at least once. Print a
4612 global localhostname
4616 stats
= {'coverfile': [0,0,0], 'banner': [0,0,0], 'fanart': [0,0,0]}
4618 graphics_file_dict
={}
4619 all_graphics_file_list
=[]
4620 for directory
in graphicsDirectories
.keys():
4621 if directory
== 'screenshot':
4623 file_list
= _getFileList(self
.config
[graphicsDirectories
[directory
]])
4624 if not len(file_list
):
4625 graphics_file_dict
[directory
] = []
4627 for g_file
in list(file_list
): # Cull the list removing dirs and non-graphics files
4628 if os
.path
.isdir(g_file
):
4629 file_list
.remove(g_file
)
4631 g_ext
= _getExtention(g_file
)
4632 if not g_ext
in self
.image_extensions
:
4633 file_list
.remove(g_file
)
4635 for filel
in file_list
:
4636 if not filel
in all_graphics_file_list
:
4637 all_graphics_file_list
.append(filel
)
4638 graphics_file_dict
[directory
] = file_list
4640 for key
in graphicsDirectories
.keys(): # Set initial totals
4641 if key
== 'screenshot':
4643 stats
[key
][num_total
] = len(graphics_file_dict
[key
])
4645 # Start reading videometadata records to remove their graphics from the image orphan list
4647 records
= mythvideo
.searchVideos()
4648 except MythError
, e
:
4649 sys
.stderr
.write(u
"\n! Error: Reading all videometadata records: %s\n" % e
.args
[0])
4652 atleast_one_video_file
= False
4654 for record
in records
:
4655 atleast_one_video_file
= True
4656 meta_dict
= {'host': record.host, 'coverfile': record.coverfile, 'banner': record.banner, 'fanart': record.fanart, 'filename': record.filename, 'intid': record.intid, 'inetref': record.inetref, }
4657 # Skip any videometadata record that is not for this host
4658 if meta_dict
['host'] != u
'' and meta_dict
['host'] != None:
4659 if meta_dict
['host'].lower() != localhostname
.lower():
4661 # Start removing any graphics in this videometadata record
4662 for key
in meta_dict
.keys():
4663 if key
in ['host','filename','intid', 'inetref']:
4665 if meta_dict
[key
] in [None, u
'', u
'None', u
'No Cover', u
'Unknown']:
4668 # Deal with videometadata record using storage groups
4669 if meta_dict
['filename'] != None:
4670 if meta_dict
['filename'][0] == u
'/':
4671 self
.absolutepath
= True
4673 self
.absolutepath
= False
4674 if meta_dict
[key
][0] != '/':
4675 meta_dict
[key
] = self
.rtnAbsolutePath(meta_dict
[key
], graphicsDirectories
[key
])
4676 if meta_dict
[key
][0] != '/': # There is not a storage group for this relative file name
4679 # Deal with TV series level graphics
4680 (dirName
, fileName
) = os
.path
.split(meta_dict
[key
])
4681 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
4682 index
= fileBaseName
.find(u
' Season ')
4683 intid
= meta_dict
['intid']
4685 if index
!= -1: # Is this a TV Series episode?
4686 if meta_dict
[key
] in graphics_file_dict
[key
]:
4687 if self
._checkValidGraphicFile
(meta_dict
[key
], graphicstype
=key
, vidintid
=intid
) == True:
4688 graphics_file_dict
[key
].remove(meta_dict
[key
])
4689 all_graphics_file_list
.remove(meta_dict
[key
])
4690 # This logic is specific to Movies and videos with a '99999999' inetref numbers
4691 elif fileName
.startswith(meta_dict
['inetref']+u
'_') or fileName
.startswith(meta_dict
['inetref']+u
'.') or meta_dict
['inetref'] == '99999999':
4692 if meta_dict
[key
] in graphics_file_dict
[key
]:
4693 if self
._checkValidGraphicFile
(meta_dict
[key
], graphicstype
=key
, vidintid
=intid
) == True:
4694 graphics_file_dict
[key
].remove(meta_dict
[key
])
4695 all_graphics_file_list
.remove(meta_dict
[key
])
4697 if not atleast_one_video_file
:
4698 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")
4700 # end reading videometadata records to remove their graphics from the image orphan list
4702 # Get Scheduled and Recorded program list
4703 programs
= self
._getScheduledRecordedProgramList
()
4705 # Remove Scheduled and Recorded program's graphics files from the delete list
4707 for field
in graphicsDirectories
.keys():
4708 if field
== 'screenshot':
4711 for graphic
in graphics_file_dict
[field
]:
4712 (dirName
, fileName
) = os
.path
.split(graphic
)
4713 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
4714 for program
in programs
:
4715 if fileBaseName
.lower().startswith(program
['title'].lower()+u
' '):
4716 remove
.append(graphic
)
4718 if not isValidPosixFilename(program
['title']) and program
['seriesid'] != u
'':
4719 if fileBaseName
.lower().startswith(program
['seriesid'].lower()):
4720 remove
.append(graphic
)
4723 if self
._checkValidGraphicFile
(rem
, graphicstype
=u
'', vidintid
=False) == True:
4724 graphics_file_dict
[field
].remove(rem
)
4726 all_graphics_file_list
.remove(rem
)
4727 except ValueError, e
:
4730 # Do not remove the MiroBridge default image files even if they are not currently being used
4731 for filel
in list(all_graphics_file_list
):
4732 if filel
.endswith('mirobridge_coverart.jpg'):
4733 all_graphics_file_list
.remove(filel
)
4735 if filel
.endswith('mirobridge_banner.jpg'):
4736 all_graphics_file_list
.remove(filel
)
4738 if filel
.endswith('mirobridge_fanart.jpg'):
4739 all_graphics_file_list
.remove(filel
)
4742 for key
in graphicsDirectories
.keys(): # Set deleted files totals
4743 if key
== 'screenshot':
4745 file_list
= list(graphics_file_dict
[key
])
4746 for filel
in file_list
:
4747 if not filel
in all_graphics_file_list
:
4748 graphics_file_dict
[key
].remove(filel
)
4749 stats
[key
][num_deleted
] = len(graphics_file_dict
[key
])
4751 # Delete all graphics files still on the delete list
4752 for filel
in all_graphics_file_list
:
4753 if self
.config
['simulation']:
4755 u
"Simulation deleting (%s)\n" % (filel
)
4762 self
._displayMessage
(u
"(%s) Has been deleted\n" % (filel
))
4764 for key
in graphicsDirectories
.keys(): # Set new files totals
4765 if key
== 'screenshot':
4767 stats
[key
][num_new_total
] = stats
[key
][num_total
] - stats
[key
][num_deleted
]
4769 if self
.config
['simulation']:
4770 sys
.stdout
.write(u
'\n\n------------Simulated Statistics---------------')
4771 sys
.stdout
.write(u
'\n--------------Janitor Statistics---------------\n')
4772 stat_type
= ['total', 'deleted', 'new total']
4773 for index
in range(len(stat_type
)):
4774 for key
in graphicsDirectories
.keys(): # Print stats
4775 if key
== 'screenshot':
4777 if key
== 'coverfile':
4781 sys
.stdout
.write(u
'% 9s % 7s ......................(% 5d)\n' % (stat_type
[index
], g_type
, stats
[key
][index
], ))
4783 for key
in graphicsDirectories
.keys(): # Print stats
4784 if key
== 'screenshot':
4786 if not len(graphics_file_dict
[key
]):
4788 if key
== 'coverfile':
4792 sys
.stdout
.write(u
'\n----------------Deleted %s files---------------\n' % g_type
)
4793 for graphic
in graphics_file_dict
[key
]:
4794 sys
.stdout
.write('%s\n' % graphic
)
4796 # end _graphicsCleanup
4798 def _getVideoLength(self
, videofilename
):
4799 '''Using ffmpeg (if it can be found) get the duration of the video
4800 return False if either ffmpeg cannot be found or the file is not a video
4801 return video lenght in minutes
4803 if not self
.config
['ffmpeg']:
4806 # Filter out specific file types due to potential negative processing overhead
4807 if _getExtention(videofilename
) in [u
'iso', u
'img', u
'VIDEO_TS', u
'm2ts', u
'vob']:
4810 p
= subprocess
.Popen(u
'ffmpeg -i "%s"' % (videofilename
), shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
4814 data
= p
.stderr
.readline()
4815 if data
.endswith('not found\n'):
4816 ffmpeg_found
= False
4818 if data
.startswith(' Duration:'):
4820 if data
== '' and p
.poll() != None:
4823 if ffmpeg_found
== False:
4824 self
.config
['ffmpeg'] = False
4827 time
= (data
[data
.index(':')+1: data
.index('.')]).strip()
4828 return (60*(int(time
[:2]))+(int(time
[3:5])))
4831 # end _getVideoLength
4834 def _getMiroVideometadataRecords(self
):
4835 """Fetches all videometadata records with an inetref of '99999999' and a category of 'Miro'. If the
4836 videometadata record has a host them it must match the lower-case of the locahostname.
4837 aborts if processing failed
4838 return and array of matching videometadata dictionary records
4840 global localhostname
4843 records
= mythvideo
.searchVideos(category
=u
'Miro', custom
=(('inetref=%s','99999999'),))
4844 except MythError
, e
:
4845 sys
.stderr
.write(u
"\n! Error: Reading all Miro videometadata records: %s\n" % e
.args
[0])
4848 for record
in records
:
4849 intids
.append(record
.intid
)
4851 videometadatarecords
=[]
4853 for intid
in intids
:
4854 vidrec
= Video(id=intid
, db
=mythvideo
)
4855 if vidrec
[u
'host'] != u
'' and vidrec
[u
'host'] != None:
4856 if vidrec
[u
'host'].lower() != localhostname
.lower():
4858 videometadatarecords
.append(vidrec
)
4860 return videometadatarecords
4863 # end _getMiroVideometadataRecords()
4865 def _getExtraMiroDetails(self
, mythvideorec
, vidtype
):
4866 '''Find the extra details required for Miro MythVideo record processing
4867 return a dictionary of details required for processing
4870 extradata
[u
'intid'] = [mythvideorec
[u
'intid']]
4871 if vidtype
== u
'movies':
4872 extradata
[u
'tv'] = False
4874 extradata
[u
'tv'] = True
4876 for key
in [u
'coverfile', u
'banner', u
'fanart', ]:
4877 extradata
[key
] = True # Set each graphics type as if it has already been downloaded
4878 if mythvideorec
[key
] == None or mythvideorec
[key
] == u
'No Cover' or mythvideorec
[key
] == u
'':
4879 extradata
[key
] = False
4881 elif key
== u
'coverfile': # Look for undersized coverart
4882 if mythvideorec
[u
'filename'][0] == u
'/':
4883 self
.absolutepath
= True
4885 self
.absolutepath
= False
4886 filename
= self
.rtnAbsolutePath(mythvideorec
[key
], graphicsDirectories
[key
])
4888 (width
, height
) = self
.config
['image_library'].open(filename
).size
4889 if width
< self
.config
['min_poster_size']:
4890 extradata
[key
] = False
4893 extradata
[key
] = False
4896 else: # Check if the default graphics are being used
4897 if mythvideorec
[key
].endswith(u
'mirobridge_banner.jpg'):
4898 extradata
[key
] = False
4899 if mythvideorec
[key
].endswith(u
'mirobridge_fanart.jpg'):
4900 extradata
[key
] = False
4903 if vidtype
== u
'movies': # Data specific to Movie Trailers
4904 if mythvideorec
[u
'filename'][0] == u
'/':
4905 self
.absolutepath
= True
4907 self
.absolutepath
= False
4908 extradata
[u
'filename'] = mythvideorec
[u
'filename']
4909 extradata
[u
'pathfilename'] = self
.rtnAbsolutePath(mythvideorec
[u
'filename'], u
'mythvideo')
4910 if os
.path
.islink(extradata
[u
'pathfilename']):
4911 extradata
[u
'symlink'] = True
4913 extradata
[u
'symlink'] = False
4914 moviename
= mythvideorec
['subtitle']
4918 index
= moviename
.find(self
.config
[u
'mb_movies'][filter(is_not_punct_char
, mythvideorec
[u
'title'].lower())])
4920 moviename
= moviename
[:index
].strip()
4921 extradata
[u
'moviename'] = moviename
4922 extradata
[u
'inetref'] = False
4923 if not moviename
== None and not moviename
== '':
4924 lastyear
= int(datetime
.datetime
.now().strftime(u
"%Y"))
4927 while i
< 5: # Check for a Movie that will be released this year or the next four years
4928 years
.append(u
"%d" % ((lastyear
+i
)))
4930 imdb_access
= imdb
.IMDb()
4933 movies_found
= imdb_access
.search_movie(moviename
.encode("ascii", 'ignore'))
4937 for movie
in movies_found
: # Get rid of duplicates
4938 if movie
.has_key('year'):
4939 temp
= {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])}
4940 if tmp_movies
.has_key(temp
.keys()[0]):
4942 tmp_movies
[temp
.keys()[0]] = temp
[temp
.keys()[0]]
4944 for movie
in tmp_movies
:
4945 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
:
4946 extradata
[u
'inetref'] = u
"%07d" % int(movie
)
4947 extradata
[u
'moviename'] = tmp_movies
[movie
]
4948 extradata
[u
'year'] = year
4950 if extradata
[u
'inetref']:
4953 # end _getExtraMiroDetails()
4955 def updateMiroVideo(self
, program
):
4956 '''Update the information in a Miro/MythVideo record
4959 global localhostname
, graphicsDirectories
4961 mirodetails
= program
[u
'miro']
4963 for intid
in mirodetails
[u
'intid']:
4965 for key
in graphicsDirectories
.keys():
4966 if key
== u
'screenshot':
4968 if mirodetails
[key
] != True and mirodetails
[key
] != False and mirodetails
[key
] != None and mirodetails
[key
] != u
'Simulated Secondary Source graphic filename place holder':
4969 # A graphics was downloaded
4970 changed_fields
[key
] = mirodetails
[key
]
4972 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
):
4973 changed_fields
[u
'inetref'] = mirodetails
[u
'inetref']
4974 changed_fields
[u
'subtitle'] = u
''
4975 changed_fields
[u
'year'] = mirodetails
[u
'year']
4976 changed_fields
[u
'banner'] = u
''
4977 (dirName
, fileName
) = os
.path
.split(mirodetails
[u
'pathfilename'])
4978 (fileBaseName
, fileExtension
) = os
.path
.splitext(fileName
)
4980 dir_list
= os
.listdir(unicode(dirName
, 'utf8'))
4981 except (UnicodeEncodeError, TypeError):
4982 dir_list
= os
.listdir(dirName
)
4985 filename
= self
.sanitiseFileName(u
'%s - Trailer %d' % (mirodetails
[u
'moviename'], index
))
4986 fullfilename
= u
'%s/%s%s' % (dirName
, filename
, fileExtension
)
4987 for flenme
in dir_list
:
4988 if fnmatch
.fnmatch(flenme
.lower(), u
'%s.*' % filename
.lower()):
4991 changed_fields
[u
'title'] = filename
4992 if self
.config
['simulation']:
4994 u
"Simulation rename Miro-MythTV movie trailer from (%s) to (%s)\n" % (mirodetails
[u
'pathfilename'], fullfilename
))
4996 os
.rename(mirodetails
[u
'pathfilename'], fullfilename
)
4997 changed_fields
[u
'filename'] = self
.rtnRelativePath(fullfilename
, u
'mythvideo')
4998 if changed_fields
[u
'filename'][0] != u
'/':
4999 changed_fields
[u
'host'] = localhostname
.lower()
5000 else: # Deal with the whole mixing Video SG and local with SG graphics mess
5001 for key
in graphicsDirectories
.keys():
5002 if key
== u
'screenshot' or not changed_fields
.has_key(key
):
5004 if changed_fields
[key
][0] == u
'/':
5007 changed_fields
.remove(key
)
5011 if len(changed_fields
):
5012 if self
.config
['simulation']:
5013 if program
['subtitle']:
5015 u
"Simulation MythTV DB update for Miro video (%s - %s)\n" % (program
['title'], program
['subtitle']))
5018 u
"Simulation MythTV DB update for Miro video (%s)\n" % (program
['title'],))
5020 Video(id=intid
, db
=mythvideo
).update(changed_fields
)
5021 # end updateMiroVideo()
5023 def _getScheduledRecordedProgramList(self
):
5024 '''Find all Scheduled and Recorded programs
5025 return array of found programs, if none then empty array is returned
5029 # Get pending recordings
5031 progs
= MythBE(backend
=mythbeconn
.hostname
, db
=mythbeconn
.db
).getUpcomingRecordings()
5032 except MythError
, e
:
5033 sys
.stderr
.write(u
"\n! Error: Getting Upcoming Recordings list: %s\n" % e
.args
[0])
5038 if prog
.title
== None:
5040 record
['title'] = prog
.title
5041 record
['subtitle'] = prog
.subtitle
5042 record
['seriesid'] = prog
.seriesid
5044 if record
['subtitle'] and prog
.airdate
!= None:
5045 record
['originalairdate'] = prog
.airdate
[:4]
5047 if prog
.year
!= '0':
5048 record
['originalairdate'] = prog
.year
5049 elif prog
.airdate
!= None:
5050 record
['originalairdate'] = prog
.airdate
[:4]
5051 for program
in programs
: # Skip duplicates
5052 if program
['title'] == record
['title']:
5055 programs
.append(record
)
5057 # Get recorded table field names:
5059 recordedlist
= MythBE(backend
=mythbeconn
.hostname
, db
=mythbeconn
.db
).getRecordings()
5060 except MythError
, e
:
5061 sys
.stderr
.write(u
"\n! Error: Getting recorded programs list: %s\n" % e
.args
[0])
5064 if not recordedlist
:
5067 recordedprogram
= {}
5068 for recordedProgram
in recordedlist
:
5070 recordedRecord
= recordedProgram
.getRecorded()
5071 except MythError
, e
:
5072 sys
.stderr
.write(u
"\n! Error: Getting recorded table record: %s\n" % e
.args
[0])
5074 if recordedRecord
.recgroup
== u
'Deleted':
5077 if recordedRecord
.title
== None:
5079 if recordedRecord
.chanid
== 9999:
5080 recorded
[u
'miro_tv'] = True
5081 recorded
[u
'title'] = recordedRecord
.title
5082 recorded
[u
'subtitle'] = recordedRecord
.subtitle
5083 recorded
[u
'seriesid'] = recordedRecord
.seriesid
5084 for program
in programs
: # Skip duplicates
5085 if program
['title'] == recorded
['title']:
5088 programs
.append(recorded
)
5089 # Get Release year for recorded movies
5090 # Get Recorded videos recordedprogram / airdate
5092 recordedDetails
= recordedRecord
.getRecordedProgram()
5093 except MythError
, e
:
5094 sys
.stderr
.write(u
"\n! Error: Getting recordedprogram table record: %s\n" % e
.args
[0])
5096 if not recordedDetails
:
5098 if not recordedDetails
.subtitle
:
5099 recordedprogram
[recordedDetails
.title
]= u
'%d' % recordedDetails
.airdate
5101 # Add release year to recorded movies
5102 for program
in programs
:
5103 if recordedprogram
.has_key(program
['title']):
5104 program
['originalairdate'] = recordedprogram
[program
['title']]
5107 # Add real names to mb_tv if they are among the recorded videos
5108 if len(self
.config
['mb_tv_channels']):
5109 for program
in programs
:
5110 programtitle
= filter(is_not_punct_char
, program
[u
'title'].lower())
5111 if programtitle
in self
.config
['mb_tv_channels'].keys():
5112 self
.config
['mb_tv_channels'][programtitle
][1] = program
[u
'title']
5114 # Check that each program has an original airdate
5115 for program
in programs
:
5116 if not program
.has_key('originalairdate'):
5117 program
['originalairdate'] = u
'0000' # Set the original airdate to zero (unknown)
5119 # If there are any Miro TV or movies to process then add them to the list
5120 if len(self
.config
['mb_tv_channels']) or len(self
.config
['mb_movies']):
5121 miromythvideorecs
= self
._getMiroVideometadataRecords
()
5122 if miromythvideorecs
:
5123 # Create array used to check for duplicates
5126 for program
in programs
:
5127 programtitle
= filter(is_not_punct_char
, program
[u
'title'].lower())
5128 if programtitle
in self
.config
['mb_tv_channels'].keys():
5129 if not program
[u
'title'] in duplicatekeys
:
5130 duplicatekeys
[program
[u
'title']] = i
5131 elif programtitle
in self
.config
['mb_movies'].keys():
5132 moviename
= program
['subtitle']
5136 index
= moviename
.find(self
.config
['mb_movies'][programtitle
])
5138 moviename
= moviename
[:index
].strip()
5139 if not moviename
in duplicatekeys
:
5140 duplicatekeys
[moviename
] = i
5143 for record
in miromythvideorecs
:
5145 program
[u
'title'] = record
[u
'title']
5146 program
[u
'subtitle'] = record
[u
'subtitle']
5147 program
[u
'originalairdate'] = record
[u
'year']
5148 recordtitle
= filter(is_not_punct_char
, record
[u
'title'].lower())
5149 if recordtitle
in self
.config
['mb_tv_channels'].keys():
5150 if not record
[u
'title'] in duplicatekeys
.keys():
5151 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'tv')
5152 duplicatekeys
[program
[u
'title']] = len(programs
)
5153 programs
.append(program
)
5154 self
.config
['mb_tv_channels'][recordtitle
][1] = record
[u
'title']
5155 elif programs
[duplicatekeys
[program
[u
'title']]].has_key(u
'miro'):
5156 programs
[duplicatekeys
[program
[u
'title']]][u
'miro'][u
'intid'].append(record
[u
'intid'])
5158 programs
[duplicatekeys
[program
[u
'title']]][u
'miro'] = self
._getExtraMiroDetails
(record
, u
'tv')
5159 elif recordtitle
in self
.config
['mb_movies'].keys():
5160 moviename
= record
['subtitle']
5164 index
= moviename
.find(self
.config
['mb_movies'][filter(is_not_punct_char
, program
[u
'title'].lower())])
5166 moviename
= moviename
[:index
].strip()
5167 if not moviename
in duplicatekeys
.keys():
5168 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5169 if program
[u
'miro'][u
'inetref']:
5170 duplicatekeys
[moviename
] = len(programs
)
5171 programs
.append(program
)
5172 elif programs
[duplicatekeys
[moviename
]].has_key(u
'miro'):
5173 programs
[duplicatekeys
[moviename
]][u
'miro'][u
'intid'].append(record
[u
'intid'])
5175 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5176 if program
[u
'miro'][u
'inetref']:
5177 programs
[duplicatekeys
[moviename
]][u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5179 # Check that each program has seriesid
5180 for program
in programs
:
5181 if not program
.has_key('seriesid'):
5182 program
['seriesid'] = u
'' # Set an empty seriesid - Generall only for Miro Videos
5183 if program
['seriesid'] == None:
5184 program
['seriesid'] = u
'' # Set an empty seriesid
5187 # end _getScheduledRecordedProgramList
5190 def _getScheduledRecordedTVGraphics(self
, program
, graphics_type
):
5191 '''Get TV show graphics for Scheduled and Recorded TV programs
5192 return None if no graphics found
5193 return fullpath and filename of downloaded graphics file
5195 if graphics_type
== 'coverfile':
5196 graphics_type
= 'poster'
5198 self
.config
['sid'] = None
5199 if self
.config
['series_name_override']:
5200 if self
.config
['series_name_override'].has_key(program
['title'].lower()):
5201 self
.config
['sid'] = self
.config
['series_name_override'][program
['title'].lower()]
5202 # Find out if there are any Series level graphics available
5203 self
.config
['toprated'] = True
5204 self
.config
['episode_name'] = None
5205 self
.config
['series_name'] = program
['title']
5206 self
.config
['season_num'] = None
5207 self
.config
['episode_num'] = None
5209 series_graphics
= self
.getGraphics(graphics_type
)
5211 if series_graphics
!= None:
5212 cfile
= { 'file_seriesname': program
['title'],
5213 'inetref': self
.config
['sid'],
5214 'seasno': self
.config
['season_num'],
5215 'epno': self
.config
['episode_num'],
5217 'filename': program
['title'],
5221 return self
._getTvdbGraphics
(cfile
, graphics_type
, toprated
=True, watched
=True)
5223 # end _getScheduledRecordedTVGraphics
5225 def _downloadScheduledRecordedGraphics(self
):
5226 '''Get Scheduled and Recorded programs and Miro vidoes get their graphics if not already
5228 return (nothing is returned)
5230 global localhostname
5232 # Initialize reporting stats
5233 total_progs_checked
= 0
5234 total_posters_found
= 0
5235 total_banners_found
= 0
5236 total_fanart_found
= 0
5237 total_posters_downloaded
= 0
5238 total_banners_downloaded
= 0
5239 total_fanart_downloaded
= 0
5241 total_miro_movies
= 0
5243 programs
= self
._getScheduledRecordedProgramList
()
5245 if not len(programs
): # Is there any programs to process?
5248 # Add any Miro Bridge mb_tv dictionary items to 'series_name_override' dictionary
5249 if not self
.config
['series_name_override'] and len(self
.config
['mb_tv_channels']):
5250 self
.config
['series_name_override'] = {}
5251 for miro_tv_key
in self
.config
['mb_tv_channels'].keys():
5252 if self
.config
['mb_tv_channels'][miro_tv_key
][0]:
5253 self
.config
['series_name_override'][self
.config
['mb_tv_channels'][miro_tv_key
][1].lower()] = self
.config
['mb_tv_channels'][miro_tv_key
][0]
5255 total_progs_checked
= len(programs
)
5257 # Get totals of Miro TV shows and movies that will be processed
5258 for program
in programs
:
5259 if program
.has_key(u
'miro'):
5260 if not program
[u
'miro'][u
'tv']:
5261 total_miro_movies
+=1
5264 elif program
.has_key(u
'miro_tv'):
5265 if filter(is_not_punct_char
, program
[u
'title'].lower()) in self
.config
['mb_movies'].keys():
5266 total_miro_movies
+=1
5270 # Prossess all TV shows and Movies
5271 for program
in programs
:
5272 program
['need'] = False # Initalize that this program does not need graphic(s) downloaded
5274 program_override_tv
= False
5275 # Check if a subtitle-less program is really a TV show with an override. This compensates for
5276 # poor EPG data sources (as has been reported from at least Australia)
5277 if not program
['subtitle'] and program
['title'].lower() in self
.config
['series_name_override']:
5279 result
= self
._searchforSeries
(program
['title'])
5280 program_override_tv
= True
5281 except Exception, e
:
5284 # Even movies get the ' Season' added to the image names so that movie such as '1408' do not clash
5285 # with TMDB#ed image names
5286 pattern
= u
'%s Season*.*'
5287 if not program
.has_key(u
'miro'):
5288 if program
['subtitle'] or program_override_tv
:
5289 graphics_name
= program
['title']
5291 if not int(program
['originalairdate']):
5292 graphics_name
= program
['title']
5294 graphics_name
= "%s (%s)" % (program
['title'], program
['originalairdate'])
5296 mirodetails
= program
[u
'miro']
5297 if mirodetails
[u
'tv']:
5298 graphics_name
= program
['title']
5300 graphics_name
= mirodetails
[u
'inetref']
5302 self
.absolutepath
= False # All Scheduled Recorded and Miro videos start in the SG "Default"
5304 # Search for graphics that are already downloaded
5305 for directory
in graphicsDirectories
.keys():
5306 if directory
== 'screenshot': # There is no downloading of screenshots required
5307 program
[directory
] = True
5309 if directory
== 'banner' and not program
['subtitle']: # No banners for movies
5310 program
[directory
] = True
5313 if not mirodetails
[u
'tv'] and directory
== 'banner': # No banners for movies
5314 program
[directory
] = True
5317 filename
= program
['title']
5318 elif mirodetails
[u
'tv']:
5319 filename
= program
['title']
5321 filename
= mirodetails
[u
'inetref']
5323 # Deal with TV series names that would generate invalid file names for images TV and movies
5324 self
.program_seriesid
= None
5325 if not isValidPosixFilename(filename
) and program
['seriesid'] != u
'':
5326 filename
= program
['seriesid']
5327 self
.program_seriesid
= program
['seriesid']
5329 # Actual check for existing graphics
5330 for dirct
in self
.config
[graphicsDirectories
[directory
]]:
5332 dir_list
= os
.listdir(unicode(dirct
, 'utf8'))
5333 except (UnicodeEncodeError, TypeError):
5334 dir_list
= os
.listdir(dirct
)
5335 for flenme
in dir_list
:
5336 if fnmatch
.fnmatch(flenme
.lower(), (pattern
% filename
).lower()):
5337 program
[directory
] = True
5338 if directory
== 'coverfile':
5339 total_posters_found
+=1
5340 elif directory
== 'banner':
5341 total_banners_found
+=1
5343 total_fanart_found
+=1
5344 if mirodetails
: # Update the Miro MythVideo records with any existing graphics
5345 mirodetails
[directory
] = self
.rtnRelativePath(u
'%s/%s' % (dirct
, flenme
), directory
)
5351 program
['need'] = True
5352 program
[directory
] = False
5354 # Check if there are any graphics to download
5355 if not program
['need']:
5357 filename
= program
['title']
5358 elif mirodetails
[u
'tv']:
5359 filename
= program
['title']
5361 filename
= mirodetails
[u
'moviename']
5362 self
._displayMessage
("All Graphics already downloaded for [%s]" % filename
)
5363 if mirodetails
: # Update the Miro MythVideo records with any new graphics
5364 self
.updateMiroVideo(program
)
5368 # It is more efficient to find inetref of movie once
5369 if not program
['subtitle'] and not program_override_tv
:
5370 if not program
.has_key('inetref'): # Was the inetref number already found?
5371 inetref
= self
._getTmdbIMDB
(graphics_name
, watched
=True)
5373 self
._displayMessage
("No movie inetref [%s]" % graphics_name
)
5374 # Fake subtitle as this may be a TV series without a subtitle
5375 program
['subtitle']=' '
5377 self
._displayMessage
("Found movie inetref (%s),[%s]" % (inetref
, graphics_name
))
5378 program
['inetref'] = inetref
5379 elif not mirodetails
[u
'tv']:
5380 program
['inetref'] = mirodetails
[u
'inetref']
5382 # Download missing graphics
5383 for key
in graphicsDirectories
.keys():
5384 if program
[key
]: # Check if this type of graphic is already downloaded
5386 miromovieflag
= False
5388 if not mirodetails
[u
'tv']:
5389 miromovieflag
= True
5390 # This is a TV episode or Miro TV show
5391 if (program
['subtitle'] or program_override_tv
) and not miromovieflag
:
5392 results
= self
._getScheduledRecordedTVGraphics
(program
, key
)
5395 filename
= program
['title']
5396 elif mirodetails
[u
'tv']:
5397 filename
= program
['title']
5399 filename
= mirodetails
[u
'moviename']
5400 if key
== 'coverfile':
5401 total_posters_downloaded
+=1
5402 elif key
== 'banner':
5403 total_banners_downloaded
+=1
5404 elif key
== 'fanart':
5405 total_fanart_downloaded
+=1
5406 if mirodetails
: # Save the filename for storing later
5407 mirodetails
[key
] = results
5409 self
._displayMessage
("TV Series - No (%s) for [%s]" % (key
, program
['title']))
5410 else: # This is a movie
5411 title
= program
['title']
5412 filename
= program
['title']
5414 title
= mirodetails
[u
'inetref']
5415 filename
= mirodetails
[u
'inetref']
5416 cfile
= { 'file_seriesname': title
,
5417 'inetref': program
['inetref'],
5421 'filename': filename
,
5425 if key
== 'coverfile':
5429 results
= self
._getTmdbGraphics
(cfile
, g_type
, watched
=True)
5431 results
= self
._getSecondarySourceGraphics
(cfile
, key
, watched
=True)
5433 if key
== 'coverfile':
5434 total_posters_downloaded
+=1
5435 elif key
== 'fanart':
5436 total_fanart_downloaded
+=1
5437 if mirodetails
: # Save the filename for storing later
5438 mirodetails
[key
] = results
5441 filename
= program
['title']
5443 filename
= mirodetails
[u
'moviename']
5444 self
._displayMessage
("No (%s) for [%s]" % (key
, filename
))
5446 if mirodetails
: # Update the Miro MythVideo records with any new graphics
5447 self
.updateMiroVideo(program
)
5450 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
))
5453 sys
.stdout
.write(u
'\n-------------Scheduled & Recorded----------\n')
5454 for program
in programs
:
5455 if not program
.has_key(u
'miro'):
5456 if program
.has_key(u
'miro_tv'):
5457 if filter(is_not_punct_char
, program
[u
'title'].lower()) in self
.config
['mb_movies'].keys():
5458 sys
.stdout
.write(u
'Miro Movie Trailer: %s\n' % (program
['title'], ))
5460 sys
.stdout
.write(u
'Miro TV Show: %s\n' % (program
['title'], ))
5462 if program
['subtitle']:
5463 sys
.stdout
.write(u
'%s\n' % (program
['title'], ))
5465 if program
['originalairdate'] != u
'0000':
5466 sys
.stdout
.write(u
'%s\n' % ("%s (%s)" % (program
['title'], program
['originalairdate'])))
5468 sys
.stdout
.write(u
'%s\n' % (program
['title'], ))
5469 elif program
[u
'miro'][u
'tv']:
5470 sys
.stdout
.write(u
'Miro TV Show: %s\n' % (program
['title'], ))
5472 sys
.stdout
.write(u
'Miro Movie Trailer: %s\n' % (program
[u
'miro'][u
'moviename'], ))
5474 # end _downloadScheduledRecordedGraphics()
5477 def findFileInDir(self
, filename
, directories
, suffix
=None, fuzzy_match
=False):
5478 '''Find if a file is in any of the specified directories. An exact match or a variation.
5479 return False - File not found in directories
5480 return True - Absolute file name and path
5482 (dirName
, fileName
) = os
.path
.split(filename
)
5483 (fileBaseName
, fileExtension
) = os
.path
.splitext(fileName
)
5484 if fuzzy_match
: # Match even when the names are not exactly the same by removing punctuation
5485 for dirct
in directories
:
5487 dir_list
= os
.listdir(unicode(dirct
, 'utf8'))
5488 except (UnicodeEncodeError, TypeError):
5489 dir_list
= os
.listdir(dirct
)
5491 for file_name
in dir_list
:
5492 match_list
.append(filter(is_not_punct_char
, file_name
.lower()))
5494 if fileBaseName
.find(suffix
) == -1:
5495 file_path
= filter(is_not_punct_char
, (u
"%s%s%s" % (fileBaseName
, suffix
, fileExtension
)).lower())
5496 file_path2
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5498 file_path
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5499 file_path2
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
.replace(suffix
, u
''), fileExtension
)).lower())
5500 if file_path
in match_list
:
5501 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path
)])
5502 if file_path2
in match_list
:
5503 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path2
)])
5506 file_path
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5507 if file_path
in match_list
:
5508 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path
)])
5511 else: # Find an exact match
5512 for directory
in directories
:
5513 if filename
[0] != u
'/' and dirName
!= u
'':
5514 dir_name
= u
"%s/%s" % (directory
, dirName
)
5516 dir_name
= directory
5518 if fileBaseName
.find(suffix
) == -1:
5519 file_path
= u
"%s/%s%s%s" % (dir_name
, fileBaseName
, suffix
, fileExtension
)
5520 file_path2
= u
'%s/%s' % (dir_name
, fileName
)
5522 file_path
= u
'%s/%s' % (dir_name
, fileName
)
5523 file_path2
= u
'%s/%s' % (dir_name
, fileName
.replace(suffix
, u
''))
5524 if os
.path
.isfile(file_path
):
5526 if os
.path
.isfile(file_path2
):
5530 file_path
= u
'%s/%s' % (dir_name
, fileName
)
5531 if os
.path
.isfile(file_path
):
5535 # end findFileInDir()
5539 num_secondary_source_graphics_downloaded
=0
5540 num_secondary_source_metadata_downloaded
=0
5542 def processMythTvMetaData(self
):
5543 '''Check each video file in the mythvideo directories download graphics files and meta data then
5544 update MythTV data base meta data with any new information.
5546 # Verify that the proper fields are present
5547 db_version
= mythdb
.settings
.NULL
.DBSchemaVer
5548 field_names
= mythvideo
.tablefields
['videometadata']
5549 for field
in ['season', 'episode', 'coverfile', 'screenshot', 'banner', 'fanart']:
5550 if not field
in field_names
:
5551 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
))
5554 # Initailize and instance to the TMDB api
5555 apikey
= "c27cb71cff5bd76e1a7a009380562c62"
5556 if self
.config
['interactive']:
5557 # themoviedb.org api key given by Travis Bell for Mythtv
5558 self
.config
['tmdb_api'] = tmdb_api
.MovieDb(apikey
,
5561 select_first
= False,
5562 debug
= self
.config
['debug_enabled'],
5564 language
= self
.config
['local_language'],
5565 search_all_languages
= True,)
5567 self
.config
['tmdb_api'] = tmdb_api
.MovieDb(apikey
,
5569 interactive
= False,
5570 select_first
= False,
5571 debug
= self
.config
['debug_enabled'],
5572 language
= self
.config
['local_language'],
5573 search_all_languages
= True,)
5575 # If there were directories specified move them and update the MythTV db meta data accordingly
5576 if self
.config
['video_dir']:
5577 if len(self
.config
['video_dir']) % 2 == 0:
5578 validFiles
= self
._moveVideoFiles
(self
.config
['video_dir'])
5579 self
.config
[u
'file_move_flag'] = False
5581 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']))
5584 # Check if only missing inetref video's should be processed
5585 if self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']:
5586 validFiles
= self
._findMissingInetref
()
5587 if validFiles
== None:
5588 sys
.stderr
.write(u
"\n! Warning: There were no missing interef video files found.\n\n")
5590 elif not len(validFiles
):
5591 sys
.stderr
.write(u
"\n! Warning: There were no missing interef video files found.\n\n")
5594 # Check if this is a Scheduled and Recorded graphics download request
5595 if self
.config
['mythtv_watched']:
5596 self
._downloadScheduledRecordedGraphics
()
5599 # Check if this is just a Janitor (clean up unused graphics files) request
5600 if self
.config
['mythtvjanitor']:
5601 self
._graphicsCleanup
()
5604 directories
=self
.config
['mythvideo']
5606 if not len(directories
):
5607 sys
.stderr
.write(u
"\n! Error: There must be a video directory specified in MythTv\n")
5612 num_fanart_downloads
=0
5613 num_posters_downloads
=0
5614 num_banners_downloads
=0
5615 num_episode_metadata_downloads
=0
5616 num_movies_using_imdb_numbers
=0
5617 num_symlinks_created
=0
5618 num_mythdb_updates
=0
5619 num_posters_below_min_size
=0
5620 videos_with_small_posters
=[]
5621 videos_using_imdb_numbers
=[]
5622 videos_updated_metadata
=[]
5625 sys
.stdout
.write(u
'Mythtv video database maintenance start: %s\n' % (datetime
.datetime
.now()).strftime("%Y-%m-%d %H:%M"))
5627 if not self
.config
['video_dir'] and not self
.config
['mythtv_inetref'] and not self
.config
['mythtv_ref_num']:
5628 allFiles
= self
._findFiles
(directories
, self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
5629 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'], movies
=True)
5631 if not len(validFiles
):
5632 sys
.stderr
.write(u
"\n! Error: No valid video files found\n")
5635 tv_series_season_format
=u
"%s/%s Season %d.%s"
5636 tv_series_format
=u
"%s/%s.%s"
5637 for cfile
in validFiles
:
5638 self
._displayMessage
(u
"\nNow processing video file (%s)(%s)(%s)\n" % (cfile
['filename'], cfile
['seasno'], cfile
['epno']))
5641 videopath
= tv_series_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
5642 # Find the MythTV meta data
5643 result
= mythvideo
.getVideo(exactfile
=videopath
)
5647 intid
= result
.intid
5649 result
= mythvideo
.getVideo(exactfile
=self
.rtnRelativePath(videopath
, u
'mythvideo'), host
=localhostname
.lower())
5652 has_metadata
= False
5654 intid
= result
.intid
5655 if result
.category
== 'none' and result
.year
== 1895:
5656 has_metadata
= False
5660 if result
.category
== 'none' and result
.year
== 1895:
5661 has_metadata
= False
5666 # Unless explicitly requested with options -MI or -MG do not add missing videos to DB
5667 if not self
.config
['interactive'] and not self
.config
['mythtv_guess']:
5669 # Create a new empty entry
5670 sys
.stdout
.write(u
"\n\nEntry does not exist in MythDB. Adding (%s).\n" % cfile
['filename'])
5671 new_rec
= {'title': cfile['file_seriesname'], 'filename': self.rtnRelativePath(videopath, u'mythvideo')}
5672 videopath
= self
.rtnRelativePath(videopath
, u
'mythvideo')
5673 if videopath
[0] == '/':
5674 intid
= Video(db
=mythvideo
).create(new_rec
).intid
5676 new_rec
['host'] = localhostname
.lower()
5677 intid
= Video(db
=mythvideo
).create(new_rec
).intid
5678 elif not has_metadata
:
5679 sys
.stdout
.write(u
"\n\nEntry exists in MythDB but category is 0 and year is 1895 (default values).\nUpdating (%s).\n" % cfile
['filename'])
5680 filename
= self
.rtnRelativePath(videopath
, u
'mythvideo')
5681 if filename
[0] == u
'/':
5682 Video(id=intid
, db
=mythvideo
).update({'filename': filename, u'host': u''}
)
5684 Video(id=intid
, db
=mythvideo
).update({'filename': filename, u'host': localhostname.lower()}
)
5685 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
5690 # Get a dictionary of the existing meta data plus a copy for update comparison
5692 vim
= Video(id=intid
, db
=mythvideo
)
5693 for key
in vim
.keys():
5694 meta_dict
[key
] = vim
[key
]
5696 # Fix a metadata record that has an incorrectly initialized inetref number value
5697 if meta_dict
['inetref'] == None:
5698 meta_dict
['inetref'] = u
'00000000'
5699 available_metadata
= dict(meta_dict
)
5701 available_metadata
['season']=cfile
['seasno']
5702 available_metadata
['episode']=cfile
['epno']
5704 if available_metadata
['title'] == u
'':
5705 available_metadata
['title'] = cfile
['file_seriesname']
5707 # Set whether a video file is stored in a Storage Group or not
5708 if available_metadata
['filename'][0] == u
'/':
5709 self
.absolutepath
= True
5711 self
.absolutepath
= False
5713 # There must be an Internet reference number. Get one for new records.
5714 if _can_int(meta_dict
['inetref']) and not meta_dict
['inetref'] == u
'00000000' and not meta_dict
['inetref'] == '':
5715 if meta_dict
['inetref'] == '99999999': # Records that are not updated by Jamu
5717 inetref
= meta_dict
['inetref']
5718 cfile
['inetref'] = meta_dict
['inetref']
5721 if not self
.config
['interactive'] and not self
.config
['mythtv_guess']:
5722 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']))
5724 inetref
= self
._getTmdbIMDB
(available_metadata
['title'])
5725 cfile
['inetref'] = inetref
5727 self
._displayMessage
(u
"themoviedb.com does not recognize the movie (%s) - Cannot update metadata - skipping\n" % available_metadata
['title'])
5728 missing_inetref
.append(available_metadata
['title'])
5730 # Only update the reference number
5731 if self
.config
['mythtv_ref_num'] or inetref
== '99999999':
5732 Video(id=intid
, db
=mythvideo
).update({'inetref': inetref}
)
5733 num_mythdb_updates
+=1
5734 videos_updated_metadata
.append(cfile
['filename'])
5735 self
._displayMessage
(u
"\nReference number (%s) added for (%s) \n" % (inetref
, cfile
['filename']))
5739 for key
in available_metadata
.keys():
5740 copy
[key
] = available_metadata
[key
]
5741 tmp_dict
= self
._getTvdbMetadata
(cfile
, copy
)
5743 self
._displayMessage
(u
"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile
['seasno'], cfile
['epno'], videopath
))
5744 missing_inetref
.append(available_metadata
['title'])
5746 inetref
= tmp_dict
['inetref']
5747 available_metadata
['title'] = tmp_dict
['title']
5748 cfile
['file_seriesname'] = tmp_dict
['title']
5749 # Only update the reference number and title
5750 if self
.config
['mythtv_ref_num'] or inetref
== '99999999':
5751 if inetref
== u
'99999999':
5752 Video(id=intid
, db
=mythvideo
).update({'inetref': inetref}
)
5754 Video(id=intid
, db
=mythvideo
).update({'inetref': inetref, 'title': tmp_dict['title']}
)
5755 num_mythdb_updates
+=1
5756 videos_updated_metadata
.append(cfile
['filename'])
5757 self
._displayMessage
(u
"\nReference number (%s) added for (%s) \n" % (inetref
, cfile
['filename']))
5759 cfile
['inetref'] = inetref
5760 available_metadata
['inetref'] = inetref
5762 if (meta_dict
['subtitle'] == None or meta_dict
['subtitle'] == '') and not movie
:
5763 tmp_subtitle
= self
._getSubtitle
(cfile
)
5764 if tmp_subtitle
== None:
5765 self
._displayMessage
(u
"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile
['seasno'], cfile
['epno'], videopath
))
5768 available_metadata
['subtitle'] = tmp_subtitle
5769 available_metadata
['title'] = self
.config
['series_name']
5770 cfile
['file_seriesname'] = self
.config
['series_name']
5772 # Check if current inetref is a IMDB#
5773 # If so then check it can be changed to tmdb#
5774 # If it can be changed then rename any graphics and update meta data
5775 if movie
and len(inetref
) == 7:
5776 self
._displayMessage
(u
"%s has IMDB# (%s)" % (available_metadata
['title'], inetref
))
5777 num_movies_using_imdb_numbers
+=1
5778 videos_using_imdb_numbers
.append(u
"%s has IMDB# (%s)" % (available_metadata
['title'], inetref
))
5779 movie_data
= self
._getTmdbMetadata
(cfile
, dict(available_metadata
))
5780 if movie_data
.has_key('inetref'):
5781 if available_metadata
['inetref'] != movie_data
['inetref']:
5782 available_metadata
['inetref'] = movie_data
['inetref']
5783 inetref
= movie_data
['inetref']
5784 cfile
['inetref'] = movie_data
['inetref']
5785 for graphic_type
in ['coverfile', 'banner', 'fanart']: # Rename graphics files
5786 if available_metadata
[graphic_type
] == None or available_metadata
[graphic_type
] == '':
5788 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5789 if os
.path
.isfile(graphic_file
):
5790 filepath
, filename
= os
.path
.split(graphic_file
)
5791 filename
, ext
= os
.path
.splitext( filename
)
5793 if self
.config
['simulation']:
5795 u
"Simulation renaming (%s) to (%s)\n" % (graphic_file
, tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
))
5798 dest
= tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
)
5800 if not os
.path
.isfile(dest
):
5801 os
.rename(graphic_file
, dest
)
5804 u
"Renaming image file (%s) to (%s) failed, error(%s)\n" % (graphic_file
, dest
, e
))
5806 self
._displayMessage
(u
"Renamed (%s) to (%s)\n" % (graphic_file
, tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
)))
5807 available_metadata
[graphic_type
]= self
.rtnRelativePath(dest
, graphicsDirectories
[graphic_type
])
5809 ###############################################################################
5810 # START of metadata Graphics logic - Checking, downloading, renaming
5811 ###############################################################################
5812 for graphic_type
in ['coverfile', 'banner', 'fanart']:
5813 ###############################################################################
5814 # START of MOVIE graphics updating
5815 ###############################################################################
5816 # Check that there are local graphics path for abs path video
5817 # An abs path video can only use the FE specified graphic directories
5818 if self
.absolutepath
:
5819 if not len(self
.config
['localpaths'][graphicsDirectories
[graphic_type
]]):
5821 graphicsdirs
= self
.config
['localpaths'][graphicsDirectories
[graphic_type
]]
5823 graphicsdirs
= self
.config
[graphicsDirectories
[graphic_type
]]
5825 if graphic_type
== 'banner':
5827 if graphic_type
== 'coverfile':
5832 undersized_graphic
= False
5833 for ext
in self
.image_extensions
:
5834 for graphicsdir
in graphicsdirs
:
5835 filename
= self
.findFileInDir(u
"%s.%s" % (inetref
, ext
), [graphicsdir
], suffix
=self
.graphic_suffix
[graphic_type
])
5838 available_metadata
[graphic_type
]=self
.rtnRelativePath(filename
, graphicsDirectories
[graphic_type
])
5839 if graphic_type
== 'coverfile':
5841 (width
, height
) = self
.config
['image_library'].open(filename
).size
5842 if width
< self
.config
['min_poster_size']:
5843 num_posters_below_min_size
+=1
5844 videos_with_small_posters
.append(cfile
['filename'])
5845 undersized_graphic
= True
5848 undersized_graphic
= True
5850 need_graphic
= False
5852 if not need_graphic
:
5855 if need_graphic
== True:
5856 dummy_graphic
= self
._getTmdbGraphics
(cfile
, g_type
)
5858 # Try secondary source if themoviedb.com did not have graphicrecord['title']
5859 if dummy_graphic
== None or undersized_graphic
== True:
5860 dummy_graphic
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5862 if dummy_graphic
!= None:
5863 available_metadata
[graphic_type
] = self
.rtnRelativePath(dummy_graphic
, graphicsDirectories
[graphic_type
])
5864 if graphic_type
== 'fanart':
5865 self
._displayMessage
(u
"Movie - Added fan art for(%s)" % cfile
['filename'])
5866 num_fanart_downloads
+=1
5868 self
._displayMessage
(u
"Movie - Added a poster for(%s)" % cfile
['filename'])
5869 num_posters_downloads
+=1
5871 # END of Movie graphics updates ###############################################
5873 ###############################################################################
5874 # START of TV Series graphics updating
5875 ###############################################################################
5876 need_graphic
= False
5877 new_format
= True # Initialize that a graphics file NEEDS a new format
5878 # Check if an existing TV series graphic file is in the old naming format
5879 if available_metadata
[graphic_type
] != None and available_metadata
[graphic_type
] != 'No Cover' and available_metadata
[graphic_type
] != '':
5880 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5881 filepath
, filename
= os
.path
.split(graphic_file
)
5882 filename
, ext
= os
.path
.splitext( filename
)
5883 if filename
.find(u
' Season ') != -1:
5887 if need_graphic
or new_format
: # Graphic does not exist or is in an old format
5888 for ext
in self
.image_extensions
:
5889 for graphicsdir
in graphicsdirs
:
5890 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)
5892 available_metadata
[graphic_type
]=self
.rtnRelativePath(filename
, graphicsDirectories
[graphic_type
])
5893 need_graphic
= False
5894 if graphic_type
== 'coverfile':
5896 (width
, height
) = self
.config
['image_library'].open(filename
).size
5897 if width
< self
.config
['min_poster_size']:
5898 num_posters_below_min_size
+=1
5899 videos_with_small_posters
.append(cfile
['filename'])
5902 undersized_graphic
= True
5905 if not need_graphic
:
5908 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5909 if not graphic_file
== None:
5910 graphic_file
= self
.findFileInDir(graphic_file
, [graphicsdir
], suffix
=self
.graphic_suffix
[graphic_type
], fuzzy_match
=True)
5911 if graphic_file
== None:
5913 if not need_graphic
: # Have graphic but may be using an old naming convention
5915 season_missing
= False
5916 suffix_missing
= False
5917 if graphic_file
.find(u
' Season ') == -1: # Check for Season
5919 season_missing
= True
5920 if graphic_file
.find(self
.graphic_suffix
[graphic_type
]) == -1:
5922 suffix_missing
= True
5924 filepath
, filename
= os
.path
.split(graphic_file
)
5925 baseFilename
, ext
= os
.path
.splitext( filename
)
5926 baseFilename
= self
.sanitiseFileName(baseFilename
)
5927 if season_missing
and suffix_missing
:
5928 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
, available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5929 elif suffix_missing
:
5930 newFilename
= u
"%s/%s%s%s" % (filepath
, baseFilename
, self
.graphic_suffix
[graphic_type
], ext
)
5931 elif season_missing
:
5932 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5933 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5934 if self
.config
['simulation']:
5936 u
"Simulation renaming (%s) to (%s)\n" % (graphic_file
, newFilename
)
5939 os
.rename(graphic_file
, newFilename
)
5940 available_metadata
[graphic_type
]= self
.rtnRelativePath(newFilename
, graphicsDirectories
[graphic_type
])
5942 available_metadata
[graphic_type
]= self
.rtnRelativePath(graphic_file
, graphicsDirectories
[graphic_type
])
5943 else: # Must see if a graphic is on thetvdb wiki
5944 if graphic_type
== 'coverfile' or graphic_type
== 'banner':
5945 available_metadata
[graphic_type
] = self
.rtnRelativePath(self
._getTvdbGraphics
(cfile
, graphic_type
), graphicsDirectories
[graphic_type
])
5946 if available_metadata
[graphic_type
] == None:
5947 tmp
= self
._getTvdbGraphics
(cfile
, graphic_type
, toprated
=True)
5949 tmp_fullfilename
= self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
])
5950 filepath
, filename
= os
.path
.split(tmp_fullfilename
)
5951 baseFilename
, ext
= os
.path
.splitext( filename
)
5952 baseFilename
= self
.sanitiseFileName(baseFilename
)
5953 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5954 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5955 if self
.config
['simulation']:
5957 u
"Simulation rename (%s) to (%s)\n" % (tmp_fullfilename
,newFilename
)
5960 self
._displayMessage
(u
"Rename existing graphic %s for series (%s)" % (graphic_type
, available_metadata
['title']))
5962 os
.rename(tmp_fullfilename
, newFilename
)
5963 if graphic_type
== 'coverfile':
5964 self
._displayMessage
("1-Added a poster for(%s)" % cfile
['filename'])
5965 num_posters_downloads
+=1
5967 self
._displayMessage
("1-Added a banner for(%s)" % cfile
['filename'])
5968 num_banners_downloads
+=1
5969 available_metadata
[graphic_type
] = self
.rtnRelativePath(newFilename
, graphicsDirectories
[graphic_type
])
5972 u
"IOError coping (%s) to (%s)\nError:(%s)\n" % (tmp_fullfilename
, newFilename
, e
))
5973 else: # Try a secondary source
5974 dummy
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5976 if graphic_type
== 'coverfile':
5977 self
._displayMessage
(u
"1-Secondary source poster for(%s)" % cfile
['filename'])
5978 num_posters_downloads
+=1
5980 self
._displayMessage
(u
"1-Secondary source banner for(%s)" % cfile
['filename'])
5981 num_banners_downloads
+=1
5982 available_metadata
[graphic_type
] = self
.rtnRelativePath(dummy
, graphicsDirectories
[graphic_type
])
5983 else: # download fanart
5984 tmp
= self
.rtnAbsolutePath(self
._getTvdbGraphics
(cfile
, graphic_type
, toprated
=True), graphicsDirectories
['fanart'])
5986 filepath
, filename
= os
.path
.split(tmp
)
5987 baseFilename
, ext
= os
.path
.splitext( filename
)
5988 baseFilename
= self
.sanitiseFileName(baseFilename
)
5989 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5990 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5991 if self
.config
['simulation']:
5993 u
"Simulation fanart rename (%s) to (%s)\n" % (tmp
, newFilename
)
5997 os
.rename(self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
]), newFilename
)
5998 available_metadata
['fanart'] = self
.rtnRelativePath(newFilename
, graphicsDirectories
['fanart'])
5999 num_fanart_downloads
+=1
6002 u
"IOError coping (%s) to (%s)\nError:(%s)\n" % (self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
]), newFilename
, e
))
6003 else: # Try a secondary source
6004 dummy
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
6006 available_metadata
['fanart'] = self
.rtnRelativePath(dummy
, graphicsDirectories
['fanart'])
6007 num_fanart_downloads
+=1
6008 # END of TV Series graphics updating
6009 ###############################################################################
6010 # END of metadata Graphics logic - Checking, downloading, renaming
6011 ###############################################################################
6013 ###############################################################################
6014 # START of metadata text logic - Checking, downloading, renaming
6015 ###############################################################################
6016 # Clean up meta data code
6018 if available_metadata
['rating'] == 'TV Show':
6019 available_metadata
['rating'] = 'NR'
6021 # Check if any meta data needs updating
6022 metadata_update
= True
6023 for key
in available_metadata
.keys():
6024 if key
in self
.config
['metadata_exclude_as_update_trigger']:
6027 if key
== 'rating' and (available_metadata
[key
] == 'NR' or available_metadata
[key
] == '' or available_metadata
[key
] == 'Unknown'):
6028 self
._displayMessage
(
6029 u
"At least (%s) needs updating\n" % (key
))
6031 if key
== 'userrating' and available_metadata
[key
] == 0.0:
6032 self
._displayMessage
(
6033 u
"At least (%s) needs updating\n" % (key
))
6035 if key
== 'length' and available_metadata
[key
] == 0:
6036 self
._displayMessage
(
6037 u
"At least (%s) needs updating\n" % (key
))
6039 if key
== 'category' and available_metadata
[key
] == 0:
6040 self
._displayMessage
(
6041 u
"At least (%s) needs updating\n" % (key
))
6043 if key
== 'year' and (available_metadata
[key
] == 0 or available_metadata
[key
] == 1895):
6044 self
._displayMessage
(
6045 u
"At least (%s) needs updating\n" % (key
))
6047 if movie
and key
== 'subtitle': # There are no subtitles in movies
6049 if movie
and key
== 'plot' and available_metadata
[key
] != None:
6050 if len(available_metadata
[key
].split(' ')) < 10:
6051 self
._displayMessage
(
6052 u
"The plot is less than 10 words check if a better plot exists\n")
6054 if key
== 'releasedate' and (available_metadata
[key
] == None or available_metadata
[key
] == date(1,1,1)):
6055 self
._displayMessage
(
6056 u
"At least (%s) needs updating\n" % (key
))
6058 if key
== 'hash' and (available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None):
6059 if (os
.path
.getsize(u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])) < 65536 * 2):
6061 self
._displayMessage
(
6062 u
"At least (%s) needs updating\n" % (key
))
6064 if available_metadata
[key
] == None or available_metadata
[key
] == '' or available_metadata
[key
] == 'None' or available_metadata
[key
] == 'Unknown':
6065 self
._displayMessage
(
6066 u
"At least (%s) needs updating\n" % (key
))
6069 metadata_update
= False
6070 if not movie
and not len(available_metadata
['inetref']) >= 5:
6071 self
._displayMessage
(
6072 u
"At least (%s) needs updating\n" % ('inetref'))
6073 metadata_update
= True
6074 # Find the video file's real duration in minutes
6076 length
= self
._getVideoLength
(u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'], ))
6080 if length
!= available_metadata
['length']:
6081 self
._displayMessage
(u
"Video file real length (%d) minutes needs updating\n" % (length
))
6082 metadata_update
= True
6085 genres_cast
={'genres': u'', 'cast': u''}
6087 copy
= dict(available_metadata
)
6089 tmp_dict
= self
._getTmdbMetadata
(cfile
, copy
)
6091 tmp_dict
= self
._getTvdbMetadata
(cfile
, copy
)
6092 num_episode_metadata_downloads
+=1
6095 for key
in ['genres', 'cast', 'countries']:
6096 if tmp_dict
.has_key(key
):
6097 genres_cast
[key
] = tmp_dict
[key
]
6098 for key
in available_metadata
.keys():
6099 if key
in self
.config
['metadata_exclude_as_update_trigger']:
6102 if not tmp_dict
.has_key(key
):
6104 if key
== 'userrating' and available_metadata
[key
] == 0.0:
6105 available_metadata
[key
] = tmp_dict
[key
]
6109 length
= self
._getVideoLength
(u
'%s/%s.%s' %(cfile
['filepath'], cfile
['filename'], cfile
['ext'], ))
6113 available_metadata
['length'] = length
6115 available_metadata
[key
] = tmp_dict
[key
]
6117 available_metadata
[key
] = tmp_dict
[key
]
6119 # Fix fields that must be prepared for insertion into data base
6120 available_metadata
['title'] = self
._make
_db
_ready
(available_metadata
['title'])
6121 available_metadata
['director'] = self
._make
_db
_ready
(available_metadata
['director'])
6122 available_metadata
['plot'] = self
._make
_db
_ready
(available_metadata
['plot'])
6123 if available_metadata
['year'] == 0:
6124 available_metadata
['year'] = 1895
6125 if available_metadata
['coverfile'] == None:
6126 available_metadata
['coverfile'] = u
'No Cover'
6127 if len(genres_cast
['genres']) and available_metadata
['category'] == 'none':
6129 genres
= genres_cast
['genres'][:genres_cast
['genres'].index(',')]
6131 genres
= genres_cast
['genres']
6132 available_metadata
['category'] = genres
6133 self
._displayMessage
(u
"Category added for (%s)(%s)" % (available_metadata
['title'], available_metadata
['category']))
6135 # Make sure graphics relative/absolute paths are set PROPERLY based
6136 # on the 'filename' field being a relative or absolute path. A filename with an absolite path
6137 # CAN ONLY have graphics baed on absolute paths.
6138 # A filename with a relative path can have mixed absolute and relative path graphic files
6139 if available_metadata
[u
'filename'][0] == u
'/':
6140 available_metadata
[u
'host'] = u
''
6141 for key
in [u
'coverfile', u
'banner', u
'fanart']:
6142 if available_metadata
[key
] != None and available_metadata
[key
] != u
'No Cover' and available_metadata
[key
] != u
'':
6143 if available_metadata
[key
][0] != u
'/':
6144 tmp
= self
.rtnAbsolutePath(available_metadata
[key
], graphicsDirectories
[key
])
6146 if key
== u
'coverfile':
6147 available_metadata
[key
] = u
'No Cover'
6149 available_metadata
[key
] = u
''
6151 available_metadata
[u
'host'] = localhostname
.lower()
6153 ###############################################################################
6154 # END of metadata text logic - Checking, downloading, renaming
6155 ###############################################################################
6157 ###############################################################################
6158 # START of metadata updating the MythVideo record when graphics or text has changed
6159 ###############################################################################
6160 # Check if any new information was found
6161 if not self
.config
['overwrite']:
6162 for key
in available_metadata
.keys():
6163 if available_metadata
[key
] != meta_dict
[key
]:
6164 if available_metadata
[key
] == u
'' and meta_dict
[key
] == None:
6166 if available_metadata
[key
] == u
'' and meta_dict
[key
] == u
'Unknown':
6169 self
._displayMessage
(
6170 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
])))
6172 self
._displayMessage
(
6173 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
])))
6176 self
._displayMessage
(
6177 u
"Nothing to update for video file(%s)\n" % cfile
['filename']
6181 if self
.config
['simulation']:
6183 u
"Simulation MythTV DB update for video file(%s)\n" % cfile
['filename']
6185 for key
in available_metadata
.keys():
6186 print key
," ", available_metadata
[key
]
6187 for key
in genres_cast
.keys():
6188 sys
.stdout
.write(u
"Key(%s):(%s)\n" % (key
, genres_cast
[key
]))
6190 sys
.stdout
.write('\n')
6192 # Clean up a few fields before updating Mythdb
6193 if available_metadata
['showlevel'] == 0: # Allows mythvideo to display this video
6194 available_metadata
['showlevel'] = 1
6195 Video(id=intid
, db
=mythvideo
).update(available_metadata
)
6196 num_mythdb_updates
+=1
6197 videos_updated_metadata
.append(cfile
['filename'])
6198 for key
in ['genres', 'cast', 'countries']:
6199 if key
== 'genres' and len(cfile
['categories']):
6200 genres_cast
[key
]+=cfile
['categories']
6201 if genres_cast
.has_key(key
):
6202 self
._addCastGenreCountry
( genres_cast
[key
], Video(id=intid
, db
=mythvideo
), key
)
6203 self
._displayMessage
(
6204 u
"Updated Mythdb for video file(%s)\n" % cfile
['filename']
6206 ###############################################################################
6207 # END of metadata updating the MythVideo record when graphics or text has changed
6208 ###############################################################################
6210 sys
.stdout
.write(u
"\nMythtv video database maintenance ends at : %s\n" % (datetime
.datetime
.now()).strftime("%Y-%m-%d %H:%M"))
6213 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
))
6215 if len(videos_updated_metadata
):
6216 sys
.stdout
.write(u
'\n--------------Updated Video Files----------\n' )
6217 for videofile
in videos_updated_metadata
:
6218 sys
.stdout
.write(u
'%s\n' % videofile
)
6219 if len(missing_inetref
):
6220 sys
.stdout
.write(u
'\n----------------No Inetref Found-----------\n' )
6221 for videofile
in missing_inetref
:
6222 sys
.stdout
.write(u
'%s\n' % videofile
)
6223 if len(videos_with_small_posters
):
6224 sys
.stdout
.write(u
'\n---------------Under sized Poster----------\n' )
6225 for videofile
in videos_with_small_posters
:
6226 sys
.stdout
.write(u
'%s\n' % videofile
)
6227 if len(videos_using_imdb_numbers
):
6228 sys
.stdout
.write(u
'\n---------------Movies with IMDB#s----------\n' )
6229 for videofile
in videos_using_imdb_numbers
:
6230 sys
.stdout
.write(u
'%s\n' % videofile
)
6232 # end processMythTvMetaData
6234 def __repr__(self
): # Just a place holder
6238 # end MythTvMetaData
6240 def simple_example():
6241 """Simple example of using jamu
6242 Displays the poster graphics URL(s) and episode meta data for the TV series Sanctuary, season 1
6244 returns None if there was no data found for the request TV series
6245 returns False if there is no TV series as specified
6246 returns a string with poster URLs and episode meta data
6248 # Get an instance of the variable configuration information set to default values
6249 configuration
= Configuration(interactive
= True, debug
= False)
6251 # Set the type of data to be returned
6252 configuration
.changeVariable('get_poster', True)
6253 configuration
.changeVariable('get_ep_meta', True)
6255 # Validate specific variables and set the TV series information
6256 configuration
.validate_setVariables(['Sanctuary', '1', '3'])
6258 # Get an instance of the tvdb process function and fetch the data
6259 process
= Tvdatabase(configuration
.config
)
6260 results
= process
.processTVdatabaseRequests()
6262 if results
!= None and results
!= False: # Print the returned data string to the stdout
6263 print process
.processTVdatabaseRequests().encode('utf8')
6264 # end simple_example
6268 """Support jamu from the command line
6271 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'>")
6273 parser
.add_option( "-b", "--debug", action
="store_true", default
=False, dest
="debug",
6274 help=u
"Show debugging info")
6275 parser
.add_option( "-u", "--usage", action
="store_true", default
=False, dest
="usage",
6276 help=u
"Display the six main uses for this jamu")
6277 parser
.add_option( "-e", "--examples", action
="store_true", default
=False, dest
="examples",
6278 help=u
"Display examples for executing the jamu script")
6279 parser
.add_option( "-v", "--version", action
="store_true", default
=False, dest
="version",
6280 help=u
"Display version and author information")
6281 parser
.add_option( "-i", "--interactive", action
="store_true", default
=False, dest
="interactive",
6282 help=u
"Interactive mode allows selection of a specific Series from a series list")
6283 parser
.add_option( "-f", "--flags_options", action
="store_true", default
=False,dest
="flags_options",
6284 help=u
"Display all variables and settings then exit")
6285 parser
.add_option( "-l", "--language", metavar
="LANGUAGE", default
=u
'en', dest
="language",
6286 help=u
"Select data that matches the specified language fall back to english if nothing found (e.g. 'es' Español, 'de' Deutsch ... etc)")
6287 parser
.add_option( "-s", "--simulation", action
="store_true", default
=False, dest
="simulation",
6288 help=u
"Simulation (dry run), no downloads are performed or data bases altered")
6289 parser
.add_option( "-t", "--toprated", action
="store_true", default
=False, dest
="toprated",
6290 help=u
"Only display/download the top rated TV Series graphics")
6291 parser
.add_option( "-d", "--download", action
="store_true", default
=False, dest
="download",
6292 help=u
"Download and save the graphics and/or meta data")
6293 parser
.add_option( "-n", "--nokeys", action
="store_true", default
=False, dest
="nokeys",
6294 help=u
"Do not add data type keys to data values when displaying data")
6295 parser
.add_option( "-m", "--maximum", metavar
="MAX", default
=None, dest
="maximum",
6296 help=u
"Limit the number of graphics per type downloaded. e.g. --maximum=6")
6297 parser
.add_option( "-o", "--overwrite", action
="store_true", default
=False, dest
="overwrite",
6298 help=u
"Overwrite any matching files already downloaded")
6299 parser
.add_option( "-C", "--user_config", metavar
="FILE", default
="", dest
="user_config",
6300 help=u
"User specified configuration variables. e.g --user_config='~/.jamu/jamu.conf'")
6301 parser
.add_option( "-F", "--filename", action
="store_true", default
=False, dest
="ret_filename",
6302 help=u
"Display a formated filename for an episode")
6303 parser
.add_option( "-U", "--update", action
="store_true", default
=False, dest
="update",
6304 help=u
"Update a meta data file if local episode meta data is older than what is available on thetvdb.com")
6305 parser
.add_option( "-D", "--mythtvdir", action
="store_true", default
=False, dest
="mythtvdir",
6306 help=u
"Store graphic files into the MythTV DB specified dirs")
6307 parser
.add_option( "-M", "--mythtvmeta", action
="store_true", default
=False, dest
="mythtvmeta",
6308 help=u
"Add/update TV series episode or movie meta data in MythTV DB")
6309 parser
.add_option( "-V", "--mythtv_verbose", action
="store_true", default
=False, dest
="mythtv_verbose",
6310 help=u
"Display verbose messages when performing MythTV metadata maintenance")
6311 parser
.add_option( "-J", "--mythtvjanitor", action
="store_true", default
=False, dest
="mythtvjanitor",
6312 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.")
6313 parser
.add_option( "-N", "--mythtvNFS", action
="store_true", default
=False, dest
="mythtvNFS",
6314 help=u
"This option overrides Jamu's restrictions on processing NFS mounted Video and/or graphic files.")
6315 parser
.add_option( "-I", "--mythtv_inetref", action
="store_true", default
=False, dest
="mythtv_inetref",
6316 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.")
6317 parser
.add_option( "-W", "--mythtv_watched", action
="store_true", default
=False, dest
="mythtv_watched",
6318 help=u
"Download graphics for Scheduled and Recorded videos. This option is ONLY active if the -M option is also selected.")
6319 parser
.add_option( "-G", "--mythtv_guess", action
="store_true", default
=False, dest
="mythtv_guess",
6320 help=u
"Guess at the inetref for a video. This option is ONLY active if the -M option is also selected.")
6321 parser
.add_option( "-S", "--selected_data", metavar
="TYPES", default
=None, dest
="selected_data",
6322 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")
6323 parser
.add_option( "-R", "--mythtv_ref_num", action
="store_true", default
=False, dest
="mythtv_ref_num",
6324 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.")
6326 opts
, series_season_ep
= parser
.parse_args()
6330 print "\nargs", series_season_ep
6332 # Set the default configuration values
6333 if opts
.mythtv_inetref
or opts
.mythtv_ref_num
:
6334 opts
.interactive
= True
6335 configuration
= Configuration(interactive
= opts
.interactive
, debug
= opts
.debug
)
6337 if opts
.usage
: # Display usage information
6338 sys
.stdout
.write(usage_txt
+'\n')
6341 if opts
.examples
: # Display example information
6342 sys
.stdout
.write(examples_txt
+'\n')
6345 if opts
.version
== True: # Display program information
6346 sys
.stdout
.write(u
"\nTitle: (%s); Version: (%s); Author: (%s)\n%s\n" % (
6347 __title__
, __version__
, __author__
, __purpose__
))
6350 # Verify that only one instance of the following options is running at any one time
6351 # Options (-M, -MW and -MG)
6356 MythLog
._setlevel
('none') # There cannot be any logging messages with non -M options
6357 if opts
.mythtvmeta
and opts
.mythtv_watched
:
6359 if opts
.mythtvmeta
and opts
.mythtv_guess
:
6361 if opts
.mythtvmeta
and opts
.mythtvjanitor
: # No instance check with the janitor option
6363 if opts
.mythtvmeta
and opts
.mythtv_inetref
: # No instance check with the interactive mode option
6365 if options
in [u
'M', u
'MW', u
'MG']:
6366 jamu_instance
= singleinstance(u
'/tmp/Jamu_%s_instance.pid' % options
)
6368 # check is another instance of Jamu is running
6370 if jamu_instance
.alreadyrunning():
6371 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
6374 # Message the user that they are using incompatible options with the -MW option
6375 if opts
.mythtvmeta
and opts
.mythtv_watched
and (opts
.mythtv_inetref
or opts
.interactive
):
6376 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'
6379 # Message the user that they are using incompatible options -R and -I or -i
6380 if opts
.mythtvmeta
and opts
.mythtv_ref_num
and opts
.mythtv_inetref
:
6381 print u
'\n! Error: The (-R) and (-I) options are mutually exclusive.\nPlease change your options and try again.\n'
6384 # Apply any command line switches
6385 configuration
.changeVariable('local_language', opts
.language
)
6386 configuration
.changeVariable('simulation', opts
.simulation
)
6387 configuration
.changeVariable('toprated', opts
.toprated
)
6388 configuration
.changeVariable('download', opts
.download
)
6389 configuration
.changeVariable('nokeys', opts
.nokeys
)
6390 configuration
.changeVariable('maximum', opts
.maximum
)
6391 configuration
.changeVariable('overwrite', opts
.overwrite
)
6392 configuration
.changeVariable('ret_filename', opts
.ret_filename
)
6393 configuration
.changeVariable('update', opts
.update
)
6394 configuration
.changeVariable('mythtvdir', opts
.mythtvdir
)
6395 configuration
.changeVariable('mythtvmeta', opts
.mythtvmeta
)
6396 configuration
.changeVariable('mythtv_inetref', opts
.mythtv_inetref
)
6397 configuration
.changeVariable('mythtv_ref_num', opts
.mythtv_ref_num
)
6398 configuration
.changeVariable('mythtv_watched', opts
.mythtv_watched
)
6399 configuration
.changeVariable('mythtv_guess', opts
.mythtv_guess
)
6400 configuration
.changeVariable('mythtv_verbose', opts
.mythtv_verbose
)
6401 configuration
.changeVariable('mythtvjanitor', opts
.mythtvjanitor
)
6402 configuration
.changeVariable('mythtvNFS', opts
.mythtvNFS
)
6403 configuration
.changeVariable('data_flags', opts
.selected_data
)
6405 # Check if the user wants to change options via a configuration file
6406 if opts
.user_config
!= '': # Did the user want to override the default config file name/location
6407 configuration
.setUseroptions(opts
.user_config
)
6409 default_config
= u
"%s/%s" % (os
.path
.expanduser(u
"~"), u
".mythtv/jamu.conf")
6410 if os
.path
.isfile(default_config
):
6411 configuration
.setUseroptions(default_config
)
6413 print u
"\nThere was no default Jamu configuration file found (%s)\n" % default_config
6415 if opts
.flags_options
: # Display option variables
6416 if len(series_season_ep
):
6417 configuration
.validate_setVariables(series_season_ep
)
6419 configuration
.validate_setVariables(['FAKE SERIES NAME','FAKE EPISODE NAME'])
6420 configuration
.displayOptions()
6423 # Validate specific variables
6424 configuration
.validate_setVariables(series_season_ep
)
6426 if configuration
.config
['mythtvmeta']:
6427 process
= MythTvMetaData(configuration
.config
)
6428 process
.processMythTvMetaData()
6429 elif configuration
.config
['video_dir']:
6430 process
= VideoFiles(configuration
.config
)
6431 results
= process
.processFileOrDirectory()
6432 if results
!= None and results
!= False:
6433 print process
.processFileOrDirectory().encode('utf8')
6435 process
= Tvdatabase(configuration
.config
)
6436 results
= process
.processTVdatabaseRequests()
6437 if results
!= None and results
!= False:
6438 print process
.processTVdatabaseRequests().encode('utf8')
6442 if __name__
== "__main__":