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
495 localhostname
= gethostname()
497 '''Create an instance of each: MythDB, MythVideo
499 MythLog
._setlevel
('none') # Some non option -M cannot have any logging on stdout
501 mythvideo
= MythVideo(mythdb
)
502 MythLog
._setlevel
('important,general')
504 print u
'\n! Warning - %s' % e
.args
[0]
505 filename
= os
.path
.expanduser("~")+'/.mythtv/config.xml'
506 if not os
.path
.isfile(filename
):
507 print u
'\n! Warning - A correctly configured (%s) file must exist\n' % filename
509 print u
'\n! Warning - Check that (%s) is correctly configured\n' % filename
511 print u
"\n! Warning - Creating an instance caused an error for one of: MythDBConn or MythVideo, error(%s)\n" % e
513 MythLog
._setlevel
('none') # Some non option -M cannot have any logging on stdout
514 mythbeconn
= MythBE(backend
=localhostname
, db
=mythdb
)
515 MythLog
._setlevel
('important,general')
517 print u
'\nWith any -M option Jamu must be run on a MythTV backend'
518 print u
'! Warning - %s' % e
.args
[0]
521 print u
"\n! Warning - MythTV python bindings could not be imported, error(%s)\n" % e
527 # Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available
529 # thetvdb.com specific modules
530 import MythTV
.ttvdb
.tvdb_ui
as tvdb_ui
531 # from tvdb_api import Tvdb
532 import MythTV
.ttvdb
.tvdb_api
as tvdb_api
533 from MythTV
.ttvdb
.tvdb_exceptions
import (tvdb_error
, tvdb_shownotfound
, tvdb_seasonnotfound
, tvdb_episodenotfound
, tvdb_episodenotfound
, tvdb_attributenotfound
, tvdb_userabort
)
535 # verify version of tvdbapi to make sure it is at least 1.0
536 if tvdb_api
.__version
__ < '1.0':
537 print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api
.__version
__
541 The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py.
542 They should have been installed along with the MythTV python bindings.
549 import MythTV
.tmdb
.tmdb_api
as tmdb_api
550 from MythTV
.tmdb
.tmdb_exceptions
import (TmdBaseError
, TmdHttpError
, TmdXmlError
, TmdbUiAbort
, TmdbMovieOrPersonNotFound
,)
553 The subdirectory "tmdb" containing the modules tmdb_api.py (v0.1.3 or greater), tmdb_ui.py,
554 tmdb_exceptions.py must have been installed with the MythTV python bindings.
559 if tmdb_api
.__version
__ < '0.1.3':
560 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
__)
565 try: # Check if the installation is equiped to directly search IMDB for movies
567 except ImportError, e
:
568 sys
.stderr
.write("\n! Error: To search for movies movies the IMDbPy library must be installed."\
569 "Check your installation's repository or check the following link."\
570 "from (http://imdbpy.sourceforge.net/?page=download)\nError:(%s)\n" % e
)
574 if imdb
.__version
__ < "3.8":
575 sys
.stderr
.write("\n! Error: You version the IMDbPy library (%s) is too old. You must use version 3.8 of higher." % imdb
.__version
__)
576 sys
.stderr
.write("Check your installation's repository or check the following link."\
577 "from (http://imdbpy.sourceforge.net/?page=download)\n")
580 class VideoTypes( DBData
):
583 setwheredat
= 'self.intid,'
584 logmodule
= 'Python VideoType'
589 c
.execute("""SELECT * FROM videotypes""")
591 for row
in c
.fetchall():
592 types
.append(VideoTypes(db
=db
, raw
=row
))
596 return "<VideoTypes '%s'>" % self
.extension
598 return str(self
).encode('utf-8')
599 def __init__(self
, id=None, ext
=None, db
=None, raw
=None):
601 DBData
.__init
__(self
, db
=db
, raw
=raw
)
603 DBData
.__init
__(self
, data
=(id,), db
=db
)
604 elif ext
is not None:
605 self
.__dict
__['where'] = 'extension=%s'
606 self
.__dict
__['wheredat'] = 'self.extension,'
607 DBData
.__init
__(self
, data
=(ext
,), db
=db
)
610 def isValidPosixFilename(name
, NAME_MAX
=255):
611 """Checks for a valid POSIX filename
613 Filename: a name consisting of 1 to {NAME_MAX} bytes used to name a file.
614 The characters composing the name may be selected from the set of
615 all character values excluding the slash character and the null byte.
616 The filenames dot and dot-dot have special meaning.
617 A filename is sometimes referred to as a "pathname component".
619 name: (base)name of the file
620 NAME_MAX: is defined in limits.h (implementation-defined constants)
621 Maximum number of bytes in a filename
622 (not including terminating null).
623 Minimum Acceptable Value: {_POSIX_NAME_MAX}
624 _POSIX_NAME_MAX: Maximum number of bytes in a filename
625 (not including terminating null).
628 More information on http://www.opengroup.org/onlinepubs/009695399/toc.htm
630 return 1<=len(name
)<= NAME_MAX
and "/" not in name
and "\000" not in name
631 # end isValidPosixFilename()
634 # Two routines used for movie title search and matching
635 def is_punct_char(char
):
636 '''check if char is punctuation char
637 return True if char is punctuation
638 return False if char is not punctuation
640 return char
in string
.punctuation
642 def is_not_punct_char(char
):
643 '''check if char is not punctuation char
644 return True if char is not punctuation
645 return False if chaar is punctuation
647 return not is_punct_char(char
)
649 def _getExtention(URL
):
650 """Get the graphic file extension from a URL
651 return the file extention from the URL
653 (dirName
, fileName
) = os
.path
.split(URL
)
654 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
655 return fileExtension
[1:]
658 def _getFileList(dst
):
659 ''' Create an array of fully qualified file names
660 return an array of file names
666 for directory
in dst
:
668 directory
= unicode(directory
, 'utf8')
669 except (UnicodeEncodeError, TypeError):
671 for filename
in os
.listdir(directory
):
672 names
.append(os
.path
.join(directory
, filename
))
674 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
))
677 for video_file
in names
:
678 if os
.path
.isdir(video_file
):
679 new_files
= _getFileList([video_file
])
680 for new_file
in new_files
:
681 file_list
.append(new_file
)
683 file_list
.append(video_file
)
688 class singleinstance(object):
690 singleinstance - based on Windows version by Dragan Jovelic this is a Linux
691 version that accomplishes the same task: make sure that
692 only a single instance of an application is running.
695 def __init__(self
, pidPath
):
697 pidPath - full path/filename where pid for running application is to be
698 stored. Often this is ./var/<pgmname>.pid
703 # See if pidFile exists
705 if os
.path
.exists(pidPath
):
707 # Make sure it is not a "stale" pidFile
710 pid
=int(open(pidPath
, 'r').read().strip())
712 # Check list of running pids, if not running it is stale so
729 if not self
.lasterror
:
731 # Write my pid into pidFile to keep multiple copies of program from
734 fp
=open(pidPath
, 'w')
735 fp
.write(str(os
.getpid()))
738 def alreadyrunning(self
):
739 return self
.lasterror
742 if not self
.lasterror
:
744 os
.unlink(self
.pidPath
)
745 # end singleinstance()
749 graphicsDirectories
= {'banner': u'bannerdir', 'screenshot': u'episodeimagedir', 'coverfile': u'posterdir', 'fanart': u'fanartdir'}
750 dir_dict
={'posterdir': "VideoArtworkDir", 'bannerdir': 'mythvideo.bannerDir', 'fanartdir': 'mythvideo.fanartDir', 'episodeimagedir': 'mythvideo.screenshotDir', 'mythvideo': 'VideoStartupDir'}
751 storagegroupnames
= {u'Videos': u'mythvideo', u'Coverart': u'posterdir', u'Banners': u'bannerdir', u'Fanart': u'fanartdir', u'Screenshots': u'episodeimagedir'}
752 storagegroups
={u'mythvideo': [], u'posterdir': [], u'bannerdir': [], u'fanartdir': [], u'episodeimagedir': []}
# The gobal dictionary is only populated with the current hosts storage group entries
753 image_extensions
= ["png", "jpg", "bmp"]
755 def getStorageGroups():
756 '''Populate the storage group dictionary with the local host's storage groups.
759 records
= mythdb
.getStorageGroup(hostname
=localhostname
)
761 for record
in records
:
762 # Only include Video, coverfile, banner, fanart, screenshot and trailers storage groups
763 if record
.groupname
in storagegroupnames
.keys():
764 dirname
= record
.dirname
766 dirname
= unicode(record
.dirname
, 'utf8')
767 except (UnicodeDecodeError):
768 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']))
769 continue # Skip any line that has non-utf8 characters in it
770 except (UnicodeEncodeError, TypeError):
772 # Strip the trailing slash so it is consistent with all other directory paths in Jamu
773 if dirname
[-1:] == u
'/':
774 storagegroups
[storagegroupnames
[record
.groupname
]].append(dirname
[:-1])
776 storagegroups
[storagegroupnames
[record
.groupname
]].append(dirname
)
779 any_storage_group
= False
780 tmp_storagegroups
= dict(storagegroups
)
781 for key
in tmp_storagegroups
.keys():
782 if len(tmp_storagegroups
[key
]):
783 any_storage_group
= True
785 del storagegroups
[key
] # Remove empty SG directory arrays
786 if any_storage_group
:
787 # Verify that each storage group is an existing local directory
788 storagegroup_ok
= True
789 for key
in storagegroups
.keys():
790 for directory
in storagegroups
[key
]:
791 if not os
.access(directory
, os
.F_OK
):
792 sys
.stderr
.write(u
"\n! Error: The local Storage group (%s) directory (%s) does not exist\n" % (key
, directory
))
793 storagegroup_ok
= False
794 if not storagegroup_ok
:
796 # end getStorageGroups
800 """Takes a string, checks if it is numeric.
803 >>> _can_int("A test")
817 """Default non-interactive UI, which auto-selects first results
819 def __init__(self
, config
, log
):
823 def selectSeries(self
, allSeries
):
826 def selectMovieOrPerson(self
, allElements
):
827 return makeDict([allElements
[0]])
832 UI_search_language
= u
''
833 UI_selectedtitle
= u
''
834 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
835 # Hard-coded here as it is realtively static, and saves another HTTP request, as
836 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
837 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',}
839 class jamu_ConsoleUI(BaseUI
):
840 """Interactively allows the user to select a show or movie from a console based UI
843 def _displaySeries(self
, allSeries_array
):
844 """Helper function, lists series with corresponding ID
846 if video_type
== u
'IMDB':
847 URL
= u
'http://www.imdb.com/title/tt'
848 URL2
= u
'http://www.imdb.com/find?s=all&q='+urllib
.quote_plus(UI_title
.encode("utf-8"))+'&x=0&y=0'
850 elif video_type
== u
'TMDB':
851 URL
= u
'http://themoviedb.org/movie/'
852 URL2
= u
'http://themoviedb.org/'
855 URL
= u
'http://thetvdb.com/index.php?tab=series&id=%s&lid=%s'
856 URL2
= u
'http://thetvdb.com/?tab=advancedsearch'
861 for index
in range(len(allSeries_array
)):
862 allSeries
[allSeries_array
[index
]['name']] = allSeries_array
[index
]
863 tmp_names
= allSeries
.keys()
867 # Find any TV Shows or Movies who's titles start with the video's title
868 for name
in tmp_names
:
869 if filter(is_not_punct_char
, name
.lower()).startswith(filter(is_not_punct_char
, UI_title
.lower())):
870 most_likely
.append(name
)
872 # IMDB can return titles that are a movies foriegn title. The titles that do not match
873 # the requested title need to be added to the end of the most likely titles list.
874 if video_type
== u
'IMDB' and len(most_likely
):
875 for name
in tmp_names
:
877 dummy
= most_likely
.index(name
)
879 most_likely
.append(name
)
882 # Remove any name that does not start with a title like the TV Show/Movie (except for IMDB)
884 for likely
in most_likely
:
889 if not video_type
== u
'IMDB':
892 # reorder the list of series and sid's
894 for key
in names
: # list all search results
895 new_array
.append(allSeries
[key
])
897 # If there is only one to select and it is an exact match then return with no interface display
898 if len(new_array
) == 1:
899 if filter(is_not_punct_char
, allSeries_array
[0]['name'].lower()) == filter(is_not_punct_char
, UI_title
.lower()):
902 # Add the ability to select the skip inetref of '99999999'
903 new_array
.append( {'sid': '99999999', 'name': u'User choses to ignore video'}
)
904 names
.append(u
'User choses to ignore video')
907 for key
in names
: # list all search results
908 i_show
+=1 # Start at more human readable number 1 (not 0)
909 if key
== u
'User choses to ignore video':
910 print u
"% 2s -> %s # %s" % (
912 '99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
915 if video_type
!= u
'IMDB' and video_type
!= u
'TMDB':
916 tmp_URL
= URL
% (allSeries
[key
]['sid'], UI_langid_dict
[UI_search_language
])
917 print u
"% 2s -> %s # %s" % (
922 print u
"% 2s -> %s # %s%s" % (
925 allSeries
[key
]['sid']
927 print u
"Direct search of %s # %s" % (
933 def selectSeries(self
, allSeries
):
934 global UI_selectedtitle
935 UI_selectedtitle
= u
''
936 allSeries
= self
._displaySeries
(allSeries
)
938 # Check for an automatic choice
939 if len(allSeries
) <= 2:
940 for series
in allSeries
:
941 if filter(is_not_punct_char
, series
['name'].lower()) == filter(is_not_punct_char
, UI_title
.lower()):
942 UI_selectedtitle
= series
['name']
945 display_total
= len(allSeries
)
947 if video_type
== u
'IMDB':
951 elif video_type
== u
'TMDB':
956 reftype
= u
'Series id'
958 refformat
= u
"%6d" # Attempt to have the most likely TV/Movies at the top of the list
960 while True: # return breaks this loop
962 print u
'Enter choice ("Enter" key equals first selection (1)) or input the %s directly, ? for help):' % reftype
964 except KeyboardInterrupt:
965 raise tvdb_userabort(u
"User aborted (^c keyboard interupt)")
967 raise tvdb_userabort(u
"User aborted (EOF received)")
969 self
.log
.debug(u
'Got choice of: %s' % (ans
))
974 selected_id
= int(ans
) - 1 # The human entered 1 as first result, not zero
975 except ValueError: # Input was not number
977 self
.log
.debug(u
'Got quit command (q)')
978 raise tvdb_userabort(u
"User aborted ('q' quit command)")
981 print u
"# Enter the number that corresponds to the correct video."
982 print u
"# Enter the %s number for the %s." % (reftype
, video_type
)
983 print u
"# ? - this help"
986 self
.log
.debug(u
'Unknown keypress %s' % (ans
))
988 self
.log
.debug(u
'Trying to return ID: %d' % (selected_id
))
990 UI_selectedtitle
= allSeries
[selected_id
]['name']
991 return allSeries
[selected_id
]
993 if len(ans
) == refsize
and reftype
!= u
'Series id':
994 UI_selectedtitle
= u
''
995 return {'name': u'User input', 'sid': ans}
996 elif reftype
== u
'Series id':
997 if len(ans
) >= refsize
:
998 UI_selectedtitle
= u
''
999 return {'name': u'User input', 'sid': ans}
1000 self
.log
.debug(u
'Invalid number entered!')
1001 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)
1002 UI_selectedtitle
= u
''
1003 self
._displaySeries
(allSeries
)
1005 #end while not valid_input
1007 def _useImageMagick(cmd
):
1008 """ Process graphics files using ImageMagick's utility 'mogrify'.
1009 >>> _useImageMagick('-resize 50% "poster.jpg"')
1013 return subprocess
.call(u
'mogrify %s > /dev/null' % cmd
, shell
=True)
1014 # end verifyImageMagick
1016 # Call a execute a command line process
1017 def callCommandLine(command
):
1018 '''Call a command line script or program. Display any errors
1019 return all stdoutput as a string
1021 p
= subprocess
.Popen(command
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
1024 data
= p
.stderr
.readline()
1026 sys
.stderr
.write(u
'%s\n' % data
)
1027 if data
== '' and p
.poll() != None:
1032 data
= p
.stdout
.readline()
1035 if data
== '' and p
.poll() != None:
1037 return returned_data
1038 # end callCommandLine
1041 # All functionality associated with configuration options
1042 class Configuration(object):
1043 """Set defaults, apply user configuration options, validate configuration settings and display the
1045 To view all available options run:
1046 >>> config = Configuration()
1047 >>> config.displayOptions()
1049 def __init__(self
, interactive
= False, debug
= False):
1050 """Initialize default configuration settings
1053 # Set all default variables
1054 self
.config
['interactive'] = interactive
1055 self
.config
['debug_enabled'] = debug
1056 self
.config
['flags_options'] = False
1057 self
.config
['local_language'] = u
'en'
1058 self
.config
['simulation'] = False
1059 self
.config
['toprated'] = False
1060 self
.config
['download'] = False
1061 self
.config
['nokeys'] = False
1062 self
.config
['maximum'] = None
1063 self
.config
['user_config'] = None
1064 self
.config
['overwrite'] = False
1065 self
.config
['update'] = False
1066 self
.config
['mythtvdir'] = False
1067 self
.config
['hd_dvd'] = ' HD - On DVD' # Used for HD DVD collection zero length video files
1068 self
.config
['dvd'] = ' - On DVD' # Used for DVD collection zero length video files
1070 self
.config
['video_dir'] = None
1071 self
.config
['recursive'] = True
1072 self
.config
['series_name'] = None
1073 self
.config
['sid'] = None
1074 self
.config
['season_num'] = None
1075 self
.config
['episode_num'] = None
1076 self
.config
['episode_name'] = None
1077 self
.config
['ret_filename'] = False
1079 # Flags for which data to perform actions on
1080 self
.config
['get_poster'] = False
1081 self
.config
['get_banner'] = False
1082 self
.config
['get_fanart'] = False
1083 self
.config
['get_ep_image'] = False
1084 self
.config
['get_ep_meta'] = False
1085 self
.config
['data_flags'] = ''
1086 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',]
1088 self
.config
['log'] = self
._initLogger
() # Setups the logger (self.log.debug() etc)
1090 # The default format of the file names (with and without episode names)
1091 self
.config
['with_ep_name'] = u
'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s'
1092 self
.config
['without_ep_name'] = u
'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s'
1093 self
.config
['ep_metadata'] = self
.config
['with_ep_name']
1095 # The default format of the graphics file names (with and without seasons and/or episode names)
1096 # The default is to use the URL's filename from thetvdb.com
1097 self
.config
['g_defaultname']=True
1098 # e.g. "Fringe - 01.jpg"
1099 self
.config
['g_series'] = u
'%(series)s - %(seq)s.%(ext)s'
1100 # e.g. "SG-1 - 07-02.jpg"
1101 self
.config
['g_season'] = u
'%(series)s - %(seasonnumber)02d-%(seq)s.%(ext)s'
1103 # Set default configuration variables
1104 # Start - Variables the user can override through option "-u" with their own file of variables
1105 self
.config
['allgraphicsdir'] = os
.getcwd()
1106 self
.config
['posterdir'] = None
1107 self
.config
['bannerdir'] = None
1108 self
.config
['fanartdir'] = None
1109 self
.config
['episodeimagedir'] = None
1110 self
.config
['metadatadir'] = None
1111 self
.config
['mythtvmeta'] = False
1112 self
.config
['myth_secondary_sources'] = {}
1113 self
.config
['posterresize'] = False
1114 self
.config
['fanartresize'] = False
1115 self
.config
['min_poster_size'] = 400
1116 self
.config
['image_library'] = False
1117 self
.config
['ffmpeg'] = True
1118 self
.config
['folderart'] = False
1119 self
.config
['metadata_exclude_as_update_trigger'] = ['intid', 'season', 'episode', 'showlevel', 'filename', 'coverfile', 'childid', 'browse', 'playcommand', 'trailer', 'host', 'screenshot', 'banner', 'fanart']
1120 self
.config
['filename_char_filter'] = u
"/%\000"
1121 self
.config
['ignore-directory'] = []
1124 # Dictionaries for Miro Bridge metadata downlods
1125 self
.config
['mb_tv_channels'] = {}
1126 self
.config
['mb_movies'] = {}
1128 # Episode data keys that you want to display or download.
1129 # This includes the order that you want them display or in the downloaded file.
1130 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']
1132 self
.config
['config_file'] = False
1133 self
.config
['series_name_override'] = False
1134 self
.config
['ep_name_massage'] = False
1135 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']
1138 # Regex pattern strings used to check for season number from directory names
1139 self
.config
['season_dir_pattern'] = [
1141 re
.compile(u
'''^.+?[ \._\-]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1143 re
.compile(u
'''^.+?[ \._\-][Ss]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1145 re
.compile(u
'''([0-9]+)[^\\/]*$''', re
.UNICODE
),
1147 re
.compile(u
'''[Ss]([0-9]+)[^\\/]*$''', re
.UNICODE
),
1151 # Set default regex pattern strings used to extract series name , season and episode numbers for file name
1152 self
.config
['name_parse'] = [
1154 re
.compile(u
'''^(.+?)[ \._\-]\[[Ss]([0-9]+?)\]_\[[Ee]([0-9]+?)\]?[^\\/]*$''', re
.UNICODE
),
1156 re
.compile(u
'''^(.+?)[ \._\-]\[?([0-9]+)x([0-9]+)[^\\/]*$''', re
.UNICODE
),
1157 # foo.s01.e01, foo.s01_e01
1158 re
.compile(u
'''^(.+?)[ \._\-][Ss]([0-9]+)[\.\- ]?[Ee]([0-9]+)[^\\/]*$''' , re
.UNICODE
),
1160 re
.compile(u
'''^(.+)[ \._\-]([0-9]{1})([0-9]{2})[\._ -][^\\/]*$''' , re
.UNICODE
),
1162 re
.compile(u
'''^(.+)[ \._\-]([0-9]{2})([0-9]{2,3})[\._ -][^\\/]*$''' , re
.UNICODE
),
1165 # regex strings to parse folder names for TV series title, season and episode numbers
1166 self
.config
['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': u'Season', u'episode': u'Episode'}
1167 self
.config
['fullname_parse_regex'] = [
1168 # Title/Season 1/01 Subtitle
1169 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1170 u
'''(?P<seasno>[0-9]+)/(?P<epno>[0-9]+).+$''',
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]+).+$''',
1174 # Title/Season 1/1x01 Subtitle
1175 # u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1176 # u'''(?P<seasno>[0-9]+)/(?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1177 # Title [xx]/Season 1/1x01 Subtitle
1178 u
'''^.+?/(?P<seriesname>[^/]+?)(?:\[.*\])*/%(season)s%(slash)s '''+
1179 u
'''(?P<seasno>[0-9]+)/(?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1180 # Title/Season 1/Title s01e01 Subtitle
1181 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1182 u
'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s [Ss][0-9]+'''+
1183 u
'''[Ee](?P<epno>[0-9]+).+$''',
1184 # Title/Season 1/Title 1x01 Subtitle
1185 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1186 u
'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))'''+
1187 u
'''[Xx](?P<epno>[0-9]+).+$''',
1188 # Title/Season 1/Episode 1 Subtitle
1189 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1190 u
'''(?P<seasno>[0-9]+)/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1191 # Title/Season 1/Season 1 Episode 1 Subtitle
1192 u
'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1193 u
'''(?P<seasno>[0-9]+)/%(season)s%(slash)s (?:(?P=seasno))%(slash)s '''+
1194 u
'''%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1195 # Title Season 1/01 Subtitle
1196 u
'''^.+?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1197 u
'''/(?P<epno>[0-9]+).+$''',
1198 # Title Season 1/s01e01 Subtitle
1199 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1200 u
'''/[Ss][0-9]+[Ee](?P<epno>[0-9]+).+''',
1201 # Title Season 1/1x01 Subtitle
1202 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1203 u
'''/(?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1204 # Title Season 1/Title s01e01 Subtitle
1205 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1206 u
'''/(?:(?P=seriesname))%(slash)s [Ss][0-9]+[Ee](?P<epno>[0-9]+).+$''',
1207 # Title Season 1/Title 1x01 Subtitle
1208 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1209 u
'''/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1210 # Title Season 1/Episode 1 Subtitle
1211 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1212 u
'''/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1213 # Title Season 1/Season 1 Episode 1 Subtitle
1214 u
'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1215 u
'''/%(season)s%(slash)s (?:(?P=seasno))%(slash)s %(episode)s%(slash)s (?P<epno>[0-9]+).+$'''
1218 # Initalize a valriable used by the -MW option
1219 self
.program_seriesid
= None
1220 self
.config
[u
'file_move_flag'] = False
1225 data_flags_table
={ 'P': 'get_poster', 'B': 'get_banner', 'F': 'get_fanart', 'I': 'get_ep_image', 'E': 'get_ep_meta'}
1228 def _initLogger(self
):
1229 """Sets up a logger using the logging module, returns a log object
1231 logger
= logging
.getLogger(u
"jamu")
1232 formatter
= logging
.Formatter(u
'%(asctime)s) %(levelname)s %(message)s')
1234 hdlr
= logging
.StreamHandler(sys
.stdout
)
1236 hdlr
.setFormatter(formatter
)
1237 logger
.addHandler(hdlr
)
1239 if self
.config
['debug_enabled']:
1240 logger
.setLevel(logging
.DEBUG
)
1242 logger
.setLevel(logging
.WARNING
)
1246 def setUseroptions(self
, useroptions
):
1247 """ Change variables through a user supplied configuration file
1248 return False and exit the script if there are issues with the configuration file values
1250 if useroptions
[0]=='~':
1251 useroptions
=os
.path
.expanduser("~")+useroptions
[1:]
1252 if os
.path
.isfile(useroptions
) == False:
1254 "\n! Error: The specified user configuration file (%s) is not a file\n" % useroptions
1257 cfg
= ConfigParser
.SafeConfigParser()
1258 cfg
.read(useroptions
)
1259 for section
in cfg
.sections():
1260 if section
[:5] == 'File ':
1261 self
.config
['config_file'] = section
[5:]
1263 if section
== 'variables':
1264 # Change variables per user config file
1265 for option
in cfg
.options(section
):
1266 if option
== 'video_file_exts' or option
== 'tmdb_genre_filter' or option
== 'metadata_exclude_as_update_trigger':
1267 tmp_list
= (cfg
.get(section
, option
).rstrip()).split(',')
1268 for i
in range(len(tmp_list
)): tmp_list
[i
] = (tmp_list
[i
].strip()).lower()
1269 self
.config
[option
] = tmp_list
1271 if option
== 'filename_char_filter':
1272 for char
in cfg
.get(section
, option
):
1273 self
.config
['filename_char_filter']+=char
1275 if option
== 'translate':
1276 s_e
= (cfg
.get(section
, option
).rstrip()).split(',')
1277 if not len(s_e
) == 2:
1279 for index
in range(len(s_e
)):
1280 s_e
[index
] = s_e
[index
].strip()
1281 self
.config
['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': s_e[0], u'episode': s_e[1]}
1284 # Ignore user settings for Myth Video and graphics file directories
1285 # when the MythTV metadata option (-M) is selected
1286 if self
.config
['mythtvmeta'] and option
in ['posterdir', 'bannerdir', 'fanartdir', 'episodeimagedir', 'mythvideo']:
1288 self
.config
[option
] = cfg
.get(section
, option
)
1290 if section
== 'regex':
1291 # Change variables per user config file
1292 for option
in cfg
.options(section
):
1293 self
.config
['name_parse'].append(re
.compile(unicode(cfg
.get(section
, option
), 'utf8'), re
.UNICODE
))
1295 if section
== 'ignore-directory':
1296 # Video directories to be excluded from Jamu processing
1297 for option
in cfg
.options(section
):
1298 self
.config
['ignore-directory'].append(unicode(cfg
.get(section
, option
), 'utf8'))
1300 if section
=='series_name_override':
1302 for option
in cfg
.options(section
):
1303 overrides
[option
] = cfg
.get(section
, option
)
1304 if len(overrides
) > 0:
1305 self
.config
['series_name_override'] = overrides
1307 if section
=='ep_name_massage':
1309 for option
in cfg
.options(section
):
1310 tmp
=cfg
.get(section
, option
).split(',')
1311 if len(tmp
)%2 and len(cfg
.get(section
, option
)) != 0:
1312 sys
.stderr
.write(u
"\n! Error: For (%s) 'ep_name_massage' values must be in pairs\n" % option
)
1316 while i
!= len(tmp
):
1317 tmp
[i
] = tmp
[i
].strip()
1318 tmp
[i
+1] = tmp
[i
+1].strip()
1319 tmp_array
.append([tmp
[i
].replace('"',''), tmp
[i
+1].replace('"','')])
1321 massage
[option
]=tmp_array
1322 if len(massage
) > 0:
1323 self
.config
['ep_name_massage'] = massage
1325 if section
== 'ep_metadata_to_download':
1326 if len(cfg
.options(section
)):
1327 if cfg
.options(section
)[0] == 'ep_include_data':
1328 tmp
=cfg
.get(section
, cfg
.options(section
)[0])
1329 overrides
=tmp
.split(',')
1330 for index
in range(len(overrides
)):
1331 x
= overrides
[index
].replace(' ','')
1335 del overrides
[index
]
1336 self
.config
['ep_include_data']=overrides
1338 if section
== 'data_flags':
1339 if len(cfg
.options(section
)):
1340 for option
in cfg
.options(section
):
1341 if cfg
.get(section
, option
).lower() != 'False'.lower():
1342 for key
in self
.data_flags_table
.keys():
1343 if option
== self
.data_flags_table
[key
]:
1344 self
.config
[option
] = True
1346 for sec
in ['movies-secondary-sources', 'tv-secondary-sources']:
1349 for option
in cfg
.options(section
):
1350 secondary
[option
] = cfg
.get(section
, option
)
1351 if len(secondary
) > 0:
1352 self
.config
['myth_secondary_sources'][sec
[:sec
.index('-')]] = secondary
1354 if section
== u
'mb_tv':
1355 # Add the channel names and their corresponding thetvdb.com id numbers
1356 for option
in cfg
.options(section
):
1357 self
.config
['mb_tv_channels'][filter(is_not_punct_char
, option
.lower())] = [cfg
.get(section
, option
), u
'']
1359 if section
== u
'mb_movies':
1360 # Add the channel names for movie trailer Channels
1361 for option
in cfg
.options(section
):
1362 self
.config
['mb_movies'][filter(is_not_punct_char
, option
.lower())] = cfg
.get(section
, option
)
1365 # Expand any home directories that are not fully qualified
1366 dirs_to_check
= [u
'bannerdir', u
'episodeimagedir', u
'metadatadir', u
'posterdir', u
'video_dir', u
'fanartdir']
1367 for item
in dirs_to_check
:
1368 if self
.config
[item
]:
1369 if item
== u
'metadatadir' and not self
.config
[item
]:
1371 if self
.config
[item
][0]=='~':
1372 self
.config
[item
]=os
.path
.expanduser("~")+self
.config
[item
][1:]
1375 def displayOptions(self
):
1376 """ Display all of the configuration values. This is used to verify that the user has the
1377 variables set as they want before running jamu live.
1379 keys
=self
.config
.keys()
1382 ################### Used to create the example configuration file "jamu-example-conf"
1383 # for key in keys: # Used to create the example configuration file "jamu-example-conf"
1384 # print "#%s: %s" % (key, self.config[key])
1389 if key
== 'log': # Do not display the logger instance it is irrelevant for display
1392 if key
== 'name_parse':
1393 print u
"%s (%d items)" % (key
, len(self
.config
[key
]))
1395 print u
"%s (%s)" % (key
, str(self
.config
[key
]))
1398 print u
"%s (%d items)" % (key
, len(self
.config
[key
]))
1400 print u
"%s:" % key
, self
.config
[key
]
1401 # end set_Userconfig
1403 def changeVariable(self
, key
, value
):
1404 """Change any configuration variable - caution no validation is preformed
1406 self
.config
[key
]=value
1407 # end changeVariable
1410 def _checkNFS(self
, dirs
, ext_filter
):
1411 '''Check if any of the files are on NFS shares. If they are then the user must be warned.
1412 return True if there are at least one file is on a NFS share.
1413 return False if no graphic files are on an NFS share.
1416 for d
in dirs
: # Get rid of Null directories
1421 global localhostname
, graphicsDirectories
1423 localip
= gethostbyname(localhostname
) # Get the local hosts IP address
1424 except Exception, e
:
1425 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
))
1428 # Get all curently mounted NFS shares
1429 tmp_mounts
= callCommandLine("mount -l | grep '//'").split('\n')
1431 for mount
in tmp_mounts
:
1433 parts
= mount
.split(' ')
1434 tmparray
=[P
for P
in parts
]
1435 if tmparray
[0].startswith('//'): # Is this a NFS share definition
1436 if not tmparray
[0].startswith(u
'//%s' % localip
) and not tmparray
[0].startswith(u
'//%s' % localhostname
):
1437 nfs
.append(tmparray
[2]) # Add an NFS mount name
1439 if not len(nfs
): # Check if there are any NFS mounts
1442 # Check if any of the directories have files on an NFS share
1443 for directory
in dirs
: # Check the base directories first
1445 if os
.path
.realpath(directory
).startswith(mount
):
1447 for directory
in dirs
: # Check the actual files
1448 file_list
= _getFileList([directory
])
1449 if not len(file_list
):
1452 for fle
in file_list
: # Make a copy of file_list
1453 tmp_list
.append(fle
)
1454 for g_file
in tmp_list
: # Cull the list removing dirs and non-extention files
1455 if os
.path
.isdir(g_file
):
1456 file_list
.remove(g_file
)
1458 g_ext
= _getExtention(g_file
)
1459 if not g_ext
.lower() in ext_filter
:
1460 file_list
.remove(g_file
)
1462 for filename
in file_list
: # Actually check each file against the NFS mounts
1464 if os
.path
.realpath(filename
).startswith(mount
):
1470 def _getMythtvDirectories(self
):
1471 """Get all graphics directories found in the MythTV DB and change their corresponding
1472 configuration values. /media/video:/media/virtual/VB_Share/Review
1474 # Stop processing if this local host has any storage groups
1475 global localhostname
, storagegroups
1476 # Make sure Jamu is being run on a MythTV backend
1478 sys
.stderr
.write(u
"\n! Error: Jamu must be run on a MythTV backend. Local host (%s) is not a MythTV backend.\n" % localhostname
)
1482 for key
in dir_dict
.keys():
1483 graphics_dir
= mythdb
.settings
[localhostname
][dir_dict
[key
]]
1484 # Only use path from MythTV if one was found
1485 self
.config
[key
] = []
1486 if key
== 'mythvideo' and graphics_dir
:
1487 tmp_directories
= graphics_dir
.split(':')
1488 if len(tmp_directories
):
1489 for i
in range(len(tmp_directories
)):
1490 tmp_directories
[i
] = tmp_directories
[i
].strip()
1491 if tmp_directories
[i
] != '':
1492 if os
.access(tmp_directories
[i
], os
.F_OK
):
1493 self
.config
[key
].append(tmp_directories
[i
])
1496 sys
.stderr
.write(u
"\n! Warning: MythTV video directory (%s) does not exist.\n" % (tmp_directories
[i
]))
1499 if key
!= 'mythvideo' and graphics_dir
:
1500 if os
.path
.os
.access(graphics_dir
, os
.F_OK
):
1501 self
.config
[key
] = [graphics_dir
]
1503 sys
.stderr
.write(u
"\n! Warning: MythTV (%s) directory (%s) does not exist.\n" % (key
, graphics_dir
))
1505 # Save the FE path settings local to this backend
1506 self
.config
['localpaths'] = {}
1507 for key
in dir_dict
.keys():
1508 self
.config
['localpaths'][key
] = []
1510 if len(self
.config
[key
]):
1511 self
.config
['localpaths'][key
] = list(self
.config
[key
])
1513 # If there is a Videos SG then there is always a Graphics SG using Videos as a fallback
1515 for key
in dir_dict
.keys():
1516 if key
== 'episodeimagedir' or key
== 'mythvideo':
1518 if storagegroups
.has_key(u
'mythvideo') and not storagegroups
.has_key(key
):
1519 storagegroups
[key
] = list(storagegroups
[u
'mythvideo']) # Set fall back
1521 # Use Storage Groups as the priority but append any FE directory settings that
1522 # are local to this BE but are not already used as a storage group
1523 if storagegroups
.has_key(u
'mythvideo'):
1524 for key
in storagegroups
.keys():
1525 self
.config
[key
] = list(storagegroups
[key
])
1526 for k
in self
.config
['localpaths'][key
]:
1527 if not k
in self
.config
[key
]:
1528 self
.config
[key
].append(k
) # Add any FE settings local directories not already included
1530 if key
== 'mythvideo':
1531 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
))
1533 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
))
1536 # Make sure there is a directory set for Videos and other graphics directories on this host
1538 for key
in dir_dict
.keys():
1539 if key
== 'episodeimagedir': # Jamu does nothing with Screenshots
1541 # The fall back graphics SG is the Videos SG directory as of changeset 22104
1542 if storagegroups
.has_key(u
'mythvideo') and not len(self
.config
[key
]):
1543 self
.config
[key
] = storagegroups
[u
'mythvideo']
1544 if not len(self
.config
[key
]):
1545 sys
.stderr
.write(u
"\n! Error: There must be a directory for Videos and each graphic type. The (%s) directory is missing.\n" % (key
))
1550 # Make sure that the directory set for Videos and other graphics directories have the proper permissions
1552 for key
in dir_dict
.keys():
1553 for directory
in self
.config
[key
]:
1554 if key
== 'episodeimagedir': # Jamu does nothing with Screenshots
1556 if key
== 'mythvideo':
1557 if not os
.access(directory
, os
.F_OK | os
.R_OK
):
1558 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
, ))
1561 if not os
.access(directory
, os
.F_OK | os
.R_OK | os
.W_OK
):
1562 sys
.stderr
.write(u
"\n! Error: The (%s) directory (%s) must be read/writable for Jamu to function.\n" % (key
, directory
, ))
1567 # Print out the video and image directories that will be used for processing
1568 if self
.config
['mythtv_verbose']:
1569 dir_types
={'posterdir': "Cover art ", 'bannerdir': 'Banners ', 'fanartdir': 'Fan art ', 'episodeimagedir': 'Screenshots', 'mythvideo': 'Video '}
1570 sys
.stdout
.write(u
"\n==========================================================================================\n")
1571 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
)
1572 sys
.stdout
.write(u
"Note: All directories are from settings in the MythDB specific to hostname (%s).\n" % localhostname
)
1573 sys
.stdout
.write(u
"Note: Screenshot directories are not listed as Jamu does not process Screenshots.\n")
1574 sys
.stdout
.write(u
"------------------------------------------------------------------------------------------\n")
1575 for key
in dir_dict
.keys():
1576 if key
== 'episodeimagedir':
1578 for directory
in self
.config
[key
]:
1580 if storagegroups
.has_key(key
):
1581 if directory
in storagegroups
[key
]:
1583 sys
.stdout
.write(u
"Type: %s - SG-%s - Directory: (%s)\n" % (dir_types
[key
], sg_flag
, directory
))
1584 sys
.stdout
.write(u
"------------------------------------------------------------------------------------------\n")
1585 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")
1586 sys
.stdout
.write(u
"==========================================================================================\n\n")
1588 if self
.config
[u
'file_move_flag']: # verify the destination directory in a move is read/writable
1591 for arg
in self
.args
:
1595 if not os
.access(arg
, os
.F_OK
):
1596 for dirct
in self
.config
['mythvideo']:
1597 if arg
.startswith(dirct
):
1598 if not os
.access(dirct
, os
.F_OK | os
.R_OK | os
.W_OK
):
1599 sys
.stderr
.write(u
"! Error: Your move destination root MythVideo directory (%s) must be read/writable for Jamu to function.\n\n" % (dirct
, ))
1603 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
, ))
1605 elif not os
.access(arg
, os
.F_OK | os
.R_OK | os
.W_OK
):
1606 sys
.stderr
.write(u
"! Error: Your move destination directory (%s) must be read/writable for Jamu to function.\n\n" % (arg
, ))
1612 # Check if any Video files are on a NFS shares
1613 if not self
.config
['mythtvNFS']: # Maybe the NFS check is to be skipped
1614 if self
._checkNFS
(self
.config
['mythvideo'], self
.config
['video_file_exts']):
1615 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")
1617 # end _getMythtvDirectories
1620 def _JanitorConflicts(self
):
1621 '''Verify that there are no conflict between the graphics directories of MythVideo and
1622 other MythTV plugins. Write an warning message if a conflict is found.
1623 return True when there is a conflict
1624 return False when there is no conflict
1626 # Except for the plugins below no other plugins have non-theme graphics
1628 # Table 'settings' fields 'GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation'
1630 # Table 'settings' fields 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir'
1632 # Table 'settings' fields 'MusicLocation'
1633 global graphicsDirectories
, localhostname
1634 tablefields
= ['GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation', 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir', 'MusicLocation', 'ScreenShotPath']
1635 returnvalue
= False # Initalize as no conflicts
1636 for field
in tablefields
:
1637 tmp_setting
= mythdb
.settings
[localhostname
][field
]
1640 settings
= tmp_setting
.split(':') # Account for multiple dirs per setting
1641 if not len(settings
):
1643 for setting
in settings
:
1644 for directory
in graphicsDirectories
.keys():
1645 if not self
.config
[graphicsDirectories
[directory
]]:
1647 # As the Janitor processes subdirectories matching must be a starts with check
1648 for direc
in self
.config
[graphicsDirectories
[directory
]]:
1649 if os
.path
.realpath(setting
).startswith(os
.path
.realpath(direc
)):
1650 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
]]) )
1653 # end _JanitorConflicts
1656 def _addMythtvUserFileTypes(self
):
1657 """Add video file types to the jamu list from the "videotypes" table
1659 # Get videotypes table field names:
1661 records
= VideoTypes
.getAll()
1662 except MythError
, e
:
1663 sys
.stderr
.write(u
"\n! Error: Reading videotypes MythTV table: %s\n" % e
.args
[0])
1667 for record
in records
:
1668 # Remove any extentions that are in Jamu's list but the user wants ignore
1670 if record
.extension
in self
.config
['video_file_exts']:
1671 self
.config
['video_file_exts'].remove(record
.extension
)
1672 if record
.extension
.lower() in self
.config
['video_file_exts']:
1673 self
.config
['video_file_exts'].remove(record
.extension
.lower())
1674 else: # Add extentions that are not in the Jamu list
1675 if not record
.extension
in self
.config
['video_file_exts']:
1676 self
.config
['video_file_exts'].append(record
.extension
)
1677 # Make sure that all video file extensions are lower case
1678 for index
in range(len(self
.config
['video_file_exts'])):
1679 self
.config
['video_file_exts'][index
] = self
.config
['video_file_exts'][index
].lower()
1680 # end _addMythtvUserFileTypes()
1683 def validate_setVariables(self
, args
):
1684 """Validate the contents of specific configuration variables
1685 return False and exit the script if an invalid configuation value is found
1687 # Fix all variables which were changed by a users configuration files
1688 # to 'None', 'False' and 'True' literals back to their intended values
1689 keys
=self
.config
.keys()
1690 types
={'None': None, 'False': False, 'True': True}
1692 for literal
in types
.keys():
1693 if self
.config
[key
] == literal
:
1694 self
.config
[key
] = types
[literal
]
1696 # Compile regex strings to parse folder names for TV series title, season and episode numbers
1697 self
.config
['fullname_parse'] = []
1698 for index
in range(len(self
.config
['fullname_parse_regex'])):
1699 self
.config
['fullname_parse'].append(re
.compile(self
.config
['fullname_parse_regex'][index
] % self
.config
['fullname_parse_season_episode_translation'], re
.UNICODE
))
1701 if self
.config
['mythtvmeta']:
1702 if mythdb
== None or mythvideo
== None:
1703 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")
1707 self
.config
['image_library'] = Image
1708 except Exception, e
:
1709 sys
.stderr
.write(u
"""\n! Error: Python Imaging Library is required for figuring out the sizes of
1710 the fetched poster images.
1712 In Debian/Ubuntu it is packaged as 'python-imaging'.
1713 http://www.pythonware.com/products/pil/\nError:(%s)\n""" % e
)
1716 if not _can_int(self
.config
['min_poster_size']):
1717 sys
.stderr
.write(u
"\n! Error: The poster minimum value must be an integer (%s)\n" % self
.config
['min_poster_size'])
1720 self
.config
['min_poster_size'] = int(self
.config
['min_poster_size'])
1722 if self
.config
['maximum'] != None:
1723 if _can_int(self
.config
['maximum']) == False:
1724 sys
.stderr
.write(u
"\n! Error: Maximum option is not an integer (%s)\n" % self
.config
['maximum'])
1727 # Detect if this is a move request
1728 self
.config
[u
'file_move_flag'] = False
1730 if os
.path
.isfile(args
[0]) or os
.path
.isdir(args
[0]) or args
[0][-1:] == '*':
1731 self
.config
[u
'file_move_flag'] = True
1732 self
.args
= list(args
)
1734 if self
.config
['mythtvdir']:
1735 if mythdb
== None or mythvideo
== None:
1736 sys
.stderr
.write(u
"\n! Error: MythTV python interface is not available\n")
1738 if self
.config
['mythtvdir'] or self
.config
['mythtvmeta']:
1739 self
._addMythtvUserFileTypes
() # add user filetypes from the "videotypes" table
1740 self
._getMythtvDirectories
()
1741 if self
.config
['mythtvjanitor']: # Check for graphic directory conflicts with other plugins
1742 if self
._JanitorConflicts
():
1744 if not self
.config
['mythtvNFS']:
1745 global graphicsDirectories
, image_extensions
1747 for key
in graphicsDirectories
:
1748 if key
!= u
'screenshot':
1749 for directory
in self
.config
[graphicsDirectories
[key
]]:
1750 dirs
.append(directory
)
1751 # Check if any Graphics files are on NFS shares
1752 if self
._checkNFS
(dirs
, image_extensions
):
1753 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")
1756 if self
.config
['posterresize'] != False or self
.config
['fanartresize'] != False:
1757 if _useImageMagick("-version"):
1758 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'])))
1761 if self
.config
['mythtvmeta'] and len(args
) == 0:
1765 sys
.stderr
.write(u
"\n! Error: At least a video directory, SID or season name must be supplied\n")
1768 if os
.path
.isfile(args
[0]) or os
.path
.isdir(args
[0]) or args
[0][-1:] == '*':
1769 self
.config
['video_dir'] = []
1771 self
.config
['video_dir'].append(unicode(arg
,'utf8'))
1772 elif not self
.config
['mythtvmeta']:
1773 if _can_int(args
[0]) and len(args
[0]) >= 5:
1774 self
.config
['sid'] = unicode(args
[0], 'utf8') # There is still a chance that this is a series name "90210"
1776 if self
.config
['series_name_override']:
1777 if self
.config
['series_name_override'].has_key(args
[0].lower()):
1778 self
.config
['sid'] = unicode((self
.config
['series_name_override'][args
[0].lower()]).strip(), 'utf8')
1780 self
.config
['series_name'] = unicode(args
[0].strip(), 'utf8')
1782 self
.config
['series_name'] = unicode(args
[0].strip(), 'utf8')
1785 sys
.stderr
.write("\n! Error: Too many arguments (%d), maximum is three.\n" % len(args
))
1786 print "! args:", args
1788 if len(args
) == 3 and _can_int(args
[1]) and _can_int(args
[2]):
1789 self
.config
['season_num'] = args
[1]
1790 self
.config
['episode_num'] = args
[2]
1791 elif len(args
) == 3:
1792 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]))
1794 elif len(args
) == 2 and _can_int(args
[1]):
1795 self
.config
['season_num'] = args
[1]
1797 if self
.config
['ep_name_massage']:
1798 if self
.config
['ep_name_massage'].has_key(self
.config
['series_name']):
1799 tmp_ep_name
=args
[1].strip()
1800 tmp_array
=self
.config
['ep_name_massage'][self
.config
['series_name']]
1801 for pair
in tmp_array
:
1802 tmp_ep_name
= tmp_ep_name
.replace(pair
[0],pair
[1])
1803 self
.config
['episode_name'] = unicode(tmp_ep_name
, 'utf8')
1805 self
.config
['episode_name'] = unicode(args
[1].strip(), 'utf8')
1807 self
.config
['episode_name'] = unicode(args
[1].strip(), 'utf8')
1809 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
1810 # Hard-coded here as it is realtively static, and saves another HTTP request, as
1811 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
1812 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"]
1814 # Validate language as specified by user
1815 if self
.config
['local_language']:
1816 if not self
.config
['local_language'] in valid_languages
:
1818 for lang
in valid_languages
: valid_langs
+= lang
+', '
1819 valid_langs
=valid_langs
[:-2]
1820 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
))
1822 global UI_search_language
1823 UI_search_language
= self
.config
['local_language']
1825 if self
.config
['data_flags']:
1826 for data_type
in self
.config
['data_flags']:
1827 if self
.data_flags_table
.has_key(data_type
):
1828 self
.config
[self
.data_flags_table
[data_type
]]=True
1829 # end validate_setVariables
1832 """Return a copy of the configuration variables
1836 # end class Configuration
1839 class Tvdatabase(object):
1840 """Process direct thetvdb.com requests
1842 def __init__(self
, configuration
):
1843 """Retrieve all configuration options and get an instance of tvdb_api which is used to
1844 access thetvdb.com wiki.
1846 self
.config
= configuration
1847 cache_dir
=u
"/tmp/tvdb_api_%s/" % os
.geteuid()
1848 if self
.config
['interactive']:
1849 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)
1851 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)
1854 # High level dictionay keys for select graphics URL(s)
1855 fanart_key
=u
'fanart'
1856 banner_key
=u
'series'
1857 poster_key
=u
'poster'
1858 season_key
=u
'season'
1859 # Lower level dictionay keys for select graphics URL(s)
1860 poster_series_key
=u
'680x1000'
1861 poster_season_key
=u
'season'
1862 fanart_hires_key
=u
'1920x1080'
1863 fanart_lowres_key
=u
'1280x720'
1864 banner_series_key
=u
'graphical'
1865 banner_season_key
=u
'seasonwide'
1866 # Type of graphics being requested
1867 poster_type
=u
'poster'
1868 fanart_type
=u
'fanart'
1869 banner_type
=u
'banner'
1870 ep_image_type
=u
'filename'
1872 def sanitiseFileName(self
, name
):
1873 '''Take a file name and change it so that invalid or problematic characters are substituted with a "_"
1874 return a sanitised valid file name
1876 if name
== None or name
== u
'':
1878 for char
in self
.config
['filename_char_filter']:
1879 name
= name
.replace(char
, u
'_')
1881 name
= u
'_'+name
[1:]
1883 # end sanitiseFileName()
1886 def _getSeriesBySid(self
, sid
):
1887 """Lookup a series via it's sid
1888 return tvdb_api Show instance
1890 seriesid
= u
'sid:' + sid
1891 if not self
.corrections
.has_key(seriesid
):
1892 self
._getShowData
(sid
)
1893 self
.corrections
[seriesid
] = sid
1894 return self
.shows
[sid
]
1895 tvdb_api
.Tvdb
.series_by_sid
= _getSeriesBySid
1896 # end _getSeriesBySid
1898 def _searchforSeries(self
, sid_or_name
):
1899 """Get TV series data by sid or series name
1900 return None if the TV show was not found
1901 return an tvdb_api instance of the TV show data if it was found
1903 if self
.config
['sid']:
1904 show
= self
.config
['tvdb_api'].series_by_sid(self
.config
['sid'])
1906 self
.config
['series_name']=show
[u
'seriesname']
1909 if self
.config
['series_name_override']:
1910 if self
.config
['series_name_override'].has_key(sid_or_name
.lower()):
1911 self
.config
['sid'] = (self
.config
['series_name_override'][sid_or_name
.lower()])
1912 show
= self
.config
['tvdb_api'].series_by_sid(self
.config
['sid'])
1914 self
.config
['series_name'] = show
[u
'seriesname']
1917 show
= self
.config
['tvdb_api'][sid_or_name
]
1919 self
.config
['series_name'] = show
[u
'seriesname']
1922 show
= self
.config
['tvdb_api'][sid_or_name
]
1924 self
.config
['series_name'] = show
[u
'seriesname']
1926 # end _searchforSeries
1928 def verifySeriesExists(self
):
1931 Series and Season or
1932 Series and Season and Episode number or
1933 Series and Episode name
1934 passed by the user exists on thetvdb.com
1935 return False and display an appropriate error if the TV data was not found
1936 return an tvdb_api instance of the TV show/season/episode data if it was found
1938 sid
=self
.config
['sid']
1939 series_name
=self
.config
['series_name']
1940 season
=self
.config
['season_num']
1941 episode
=self
.config
['episode_num']
1942 episode_name
=self
.config
['episode_name']
1944 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
))
1945 if episode_name
: # Find an exact match for the series and episode name
1948 seriesfound
=self
._searchforSeries
(sid
).search(episode_name
)
1950 seriesfound
=self
._searchforSeries
(series_name
).search(episode_name
)
1951 if len(seriesfound
) != 0:
1952 for ep
in seriesfound
:
1953 if ep
['seriesid'] == '999999999':
1954 self
.config
['sid'] = ep
['seriesid']
1956 if (ep
['episodename'].lower()).startswith(episode_name
.lower()):
1957 if len(ep
['episodename']) > (len(episode_name
)+1):
1958 # Skip episodes the are not part of a set of (1), (2) ... etc
1959 if ep
['episodename'][len(episode_name
):len(episode_name
)+2] != ' (':
1961 series_sid
= ep
['seriesid']
1962 self
.config
['sid'] = ep
['seriesid']
1963 self
.config
['season_num'] = ep
['seasonnumber']
1964 self
.config
['episode_num'] = ep
['episodenumber']
1967 series_sid
= ep
['seriesid']
1968 self
.config
['sid'] = ep
['seriesid']
1969 self
.config
['season_num'] = ep
['seasonnumber']
1970 self
.config
['episode_num'] = ep
['episodenumber']
1972 raise tvdb_episodenotfound
1973 # Search for the series or series & season or series & season & episode
1975 if episode
: # series & season & episode
1976 seriesfound
=self
._searchforSeries
(series_name
)[int(season
)][int(episode
)]
1977 if seriesfound
['seriesid'] == '999999999':
1979 self
.config
['sid'] = seriesfound
['seriesid']
1980 self
.config
['episode_name'] = seriesfound
['episodename']
1981 else: # series & season
1982 seriesfound
=self
._searchforSeries
(series_name
)[int(season
)]
1984 seriesfound
=self
._searchforSeries
(series_name
) # Series only
1985 except tvdb_shownotfound
:
1986 # No such show found.
1987 # Use the show-name from the files name, and None as the ep name
1989 sys
.stderr
.write(u
"\n! Warning: Series (%s) not found\n" % (
1993 sys
.stderr
.write(u
"\n! Warning: Series TVDB number (%s) not found\n" % (
1997 except (tvdb_seasonnotfound
, tvdb_episodenotfound
, tvdb_attributenotfound
):
1998 # The season, episode or name wasn't found, but the show was.
1999 # Use the corrected show-name, but no episode name.
2000 if series_name
== None:
2003 sys
.stderr
.write(u
"\n! Warning: For Series (%s), season (%s) or Episode (%s) not found \n"
2004 % (series_name
, season
, episode
)
2007 sys
.stderr
.write(u
"\n! Warning: For Series (%s), Episode (%s) not found \n"
2008 % (series_name
, episode_name
)
2011 sys
.stderr
.write(u
"\n! Warning: For Series (%s), season (%s) not found \n" % (
2012 series_name
, season
)
2015 except tvdb_error
, errormsg
:
2016 # Error communicating with thetvdb.com
2017 if sid
: # Maybe the 5 digit number was a series name (e.g. 90210)
2018 self
.config
['series_name']=self
.config
['sid']
2019 self
.config
['sid'] = None
2020 return self
.verifySeriesExists()
2022 u
"\n! Warning: Error contacting www.thetvdb.com:\n%s\n" % (errormsg
)
2025 except tvdb_userabort
, errormsg
:
2026 # User aborted selection (q or ^c)
2027 print "\n", errormsg
2031 # end verifySeriesExists
2033 def _resizeGraphic(self
, filename
, resize
):
2034 """Resize a graphics file
2035 return False and display an error message if the graphics resizing failed
2036 return True if the resize was succcessful
2038 if self
.config
['simulation']:
2040 u
"Simulation resize command (mogrify -resize %s %s)\n" % (resize
, filename
)
2043 if _useImageMagick('-resize %s "%s"' % (resize
, filename
)):
2045 u
'\n! Warning: Resizing failed command (mogrify -resize %s "%s")\n' % (resize
, filename
)
2049 # end _resizeGraphic
2051 def _downloadURL(self
, url
, OutputFileName
):
2052 """Download the specified graphic file from a URL
2053 return False if no file was downloaded
2054 return True if a file was successfully downloaded
2056 # Only download a file if it does not exist or the option overwrite is selected
2057 if not self
.config
['overwrite'] and os
.path
.isfile(OutputFileName
):
2060 if self
.config
['simulation']:
2062 u
"Simulation download of URL(%s) to File(%s)\n" % (url
, OutputFileName
)
2067 tmp_URL
= url
.replace("http://", "")
2068 url
= "http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
2071 dat
= urllib
.urlopen(url
).read()
2073 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
))
2077 target_socket
= open(OutputFileName
, "wb")
2078 target_socket
.write(dat
)
2079 target_socket
.close()
2081 sys
.stderr
.write( u
"\n! Warning: Download IOError for Filename(%s), may be the directory is invalid\nError:(%s)\n" % (OutputFileName
, e
))
2084 # Verify that the downloaded file was NOT HTML instead of the intended file
2086 p
= subprocess
.Popen(u
'file "%s"' % OutputFileName
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
2087 except Exception, e
:
2088 sys
.stderr
.write( u
"\n! Warning: Download Exception for Filename(%s)\nError:(%s)\n" % (OutputFileName
, e
))
2092 data
= p
.stdout
.readline()
2094 data
= data
.encode('utf8')
2095 except UnicodeDecodeError:
2096 data
= unicode(data
,'utf8')
2097 index
= data
.find(u
'HTML document text')
2101 os
.remove(OutputFileName
) # Delete the useless HTML text
2102 if self
.config
['mythtv_verbose']:
2103 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
))
2107 def _setGraphicsFileNameFormat(self
):
2108 """Return a file name format (e.g. seriesname - episode name.extention)
2109 return a filename format string
2111 if self
.config
['g_defaultname']:
2112 return u
'%(url)s.%(ext)s'
2114 cfile
['seriesid']=self
.config
['sid']
2115 cfile
['series'] = self
.sanitiseFileName(self
.config
['series_name'])
2116 if cfile
['series'] != self
.config
['series_name']:
2117 self
.config
['g_series'] = self
.config
['g_series'].replace(self
.config
['series_name'], cfile
['series'])
2118 if self
.config
['season_num']:
2119 cfile
['seasonnumber']=int(self
.config
['season_num'])
2121 cfile
['seasonnumber']=0
2122 if self
.config
['episode_num']:
2123 cfile
['episodenumber']=int(self
.config
['episode_num'])
2125 cfile
['episodenumber']=0
2126 cfile
['episodename']=self
.config
['episode_name']
2127 cfile
['seq']=u
'%(seq)02d'
2128 cfile
['ext']=u
'%(ext)s'
2130 if self
.config
['season_num']:
2131 return self
.config
['g_season'] % cfile
2133 return self
.config
['g_series'] % cfile
2134 # end _setGraphicsFileNameFormat
2136 def _downloadGraphics(self
, urls
, mythtv
=False):
2137 """Download graphic file(s) from a URL list (string of one or more URLs separated by a CR
2139 return None is the string of urls has no urls
2140 return False if the any of the urls are corrupt
2141 return file name of the LAST file downloaded (special for MythTV data base updates)
2143 global graphicsDirectories
2145 if urls
== None: return None
2146 if urls
== '': return None
2147 tmp_list
=urls
.split('\n')
2153 if not len(url_list
):
2154 return None # There were no URLs in the list
2158 self
.config
['log'].debug(u
'Checking for a key in (%s)' % (x
))
2162 u
"\n! Warning: URL list does not have a graphics type key(%s)\n" % (x
)
2165 if url_dict
.has_key(x
[:i
]):
2166 temp_array
= [x
[i
+1:],'']
2167 url_dict
[x
[:i
]].append(temp_array
)# Collect a list of the same graphics type of URLs
2168 else: # The first URL of a new graphics type. Also URL replacement code left in place just in case
2169 url_dict
[x
[:i
]]=[[(x
[i
+1:]).replace(u
"http://www.thetvdb.com",u
"http://www.thetvdb.com"),'']]
2171 unique_dir
={u'poster': ['posterdir', True], u'banner': ['bannerdir', True], u'fanart': ['fanartdir', True], u'filename': ['episodeimagedir', True]}
2172 # If a graphics directory was not specified then default to the 'allgraphics' directory
2173 if not self
.config
['posterdir']: self
.config
['posterdir'] = self
.config
['allgraphicsdir']
2174 if not self
.config
['bannerdir']: self
.config
['bannerdir'] = self
.config
['allgraphicsdir']
2175 if not self
.config
['fanartdir']: self
.config
['fanartdir'] = self
.config
['allgraphicsdir']
2176 if not self
.config
['episodeimagedir']: self
.config
['episodeimagedir'] = self
.config
['allgraphicsdir']
2178 # Check if any of the downloaded graphics will share the same directory
2179 for key
in unique_dir
.keys():
2180 for k
in unique_dir
.keys():
2182 if self
.config
[unique_dir
[key
][0]] == self
.config
[unique_dir
[k
][0]]:
2183 unique_dir
[key
][1] = False
2186 dirs
={u
'poster': self
.config
['posterdir'], u
'banner': self
.config
['bannerdir'],
2187 u
'fanart': self
.config
['fanartdir'], u
'filename': self
.config
['episodeimagedir']}
2189 # Figure out filenaming convention
2190 file_format
= self
._setGraphicsFileNameFormat
()
2192 # Set the graphics fully qualified filenames matched to a URL
2193 for URLtype
in url_dict
:
2195 if self
.absolutepath
:
2196 if URLtype
== 'poster':
2197 tmpgraphicdir
= graphicsDirectories
['coverfile']
2199 tmpgraphicdir
= graphicsDirectories
[URLtype
]
2200 if not len(self
.config
['localpaths'][tmpgraphicdir
]):
2203 directory
= self
.config
['localpaths'][tmpgraphicdir
][0]
2205 directory
= dirs
[URLtype
][0]
2207 directory
= dirs
[URLtype
]
2209 for url
in url_dict
[URLtype
]:
2210 (dirName
, fileName
) = os
.path
.split(url
[0])
2211 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
2212 fileBaseName
= self
.sanitiseFileName(fileBaseName
)
2213 # Fix file extentions in all caps or 4 character JPEG extentions
2214 fileExtension
= fileExtension
.lower()
2215 if fileExtension
== '.jpeg':
2216 fileExtension
= '.jpg'
2217 cfile
={u'url': fileBaseName, u'seq': seq_num, u'ext': fileExtension[1:]}
2218 if not isValidPosixFilename(self
.config
['series_name']):
2219 if file_format
.startswith(self
.config
['series_name']):
2220 file_format
= file_format
.replace(self
.config
['series_name'], self
.sanitiseFileName(self
.config
['series_name']))
2221 cfile
['series'] = self
.sanitiseFileName(self
.config
['series_name'])
2222 cfile
['seriesid'] = self
.config
['sid']
2224 if URLtype
!= 'filename':
2225 if unique_dir
[URLtype
][1]:
2226 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+file_format
% cfile
2229 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+file_format
% cfile
2231 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+URLtype
.capitalize()+' - '+file_format
% cfile
2233 if self
.config
['season_num']:
2234 cfile
['seasonnumber']=int(self
.config
['season_num'])
2236 cfile
['seasonnumber'] = 0
2237 if self
.config
['episode_num']:
2238 cfile
['episodenumber']=int(self
.config
['episode_num'])
2240 cfile
['episodenumber'] = 0
2241 cfile
['episodename'] = self
.sanitiseFileName(self
.config
['episode_name'])
2242 url_dict
[URLtype
][seq_num
][1] = directory
+'/'+self
.config
['ep_metadata'] % cfile
2245 # Download the graphics and resize if requested - Ignore download or resize issues!
2246 failed_download
= False
2247 for URLtype
in url_dict
:
2249 for pairs
in url_dict
[URLtype
]:
2250 if self
._downloadURL
(pairs
[0], pairs
[1]):
2251 if URLtype
== u
'poster' and self
.config
['posterresize']:
2252 self
._resizeGraphic
(pairs
[1], self
.config
['posterresize'])
2253 elif URLtype
== u
'fanart' and self
.config
['fanartresize']:
2254 self
._resizeGraphic
(pairs
[1], self
.config
['fanartresize'])
2255 elif not os
.path
.isfile(pairs
[1]): # Check if the file already was downloaded
2256 failed_download
= True # The download failed
2257 if self
.config
['mythtv_verbose']:
2258 sys
.stderr
.write(u
'\nA graphics file failed to be downloaded. A file issue or a corrupt (HTML) file.(%s)\n' % pairs
[1])
2260 if self
.config
['maximum']: # Has the maximum number of graphics been downloaded?
2261 if seq_num
== int(self
.config
['maximum']):
2266 return pairs
[1] # The name of the LAST graphics successfully downloaded
2267 # end _downloadGraphics
2269 def getGraphics(self
, graphics_type
):
2270 """Retrieve Poster or Fan Art or Banner or Episode image graphics URL(s)
2271 return None if no graphics URLs were found
2272 return a string of URLs
2275 series_name
=self
.config
['series_name']
2276 season
=self
.config
['season_num']
2277 episode
=self
.config
['episode_num']
2278 episode_name
=self
.config
['episode_name']
2279 lang
=self
.config
['local_language']
2283 if self
.config
['sid']:
2284 URLs
= self
.config
['tvdb_api'].ttvdb_parseBanners(self
.config
['sid'])
2286 URLs
= self
.config
['tvdb_api'].ttvdb_parseBanners(self
.config
['tvdb_api']._nameToSid
(series_name
))
2287 except Exception, e
:
2290 if graphics_type
== self
.fanart_type
: # Series fanart graphics
2291 if not len(URLs
[u
'fanart']):
2293 for url
in URLs
[u
'fanart']:
2294 graphics
.append(url
)
2295 elif season
== None and episode
== None and episode_name
== None:
2296 if not len(URLs
[u
'series']):
2298 if graphics_type
== self
.banner_type
: # Series Banners
2299 for url
in URLs
[u
'series']:
2300 graphics
.append(url
)
2301 else: # Series Posters
2302 for url
in URLs
[u
'poster']:
2303 graphics
.append(url
)
2305 if not len(URLs
[u
'season']):
2307 if graphics_type
== self
.banner_type
: # Season Banners
2309 for url
in URLs
[u
'season']:
2310 if url
[u
'bannertype2'] == u
'seasonwide' and url
[u
'season'] == season
:
2311 season_banners
.append(url
)
2312 if not len(season_banners
):
2314 graphics
= season_banners
2315 else: # Season Posters
2317 for url
in URLs
[u
'season']:
2318 if url
[u
'bannertype2'] == u
'season' and url
[u
'season'] == season
:
2319 season_posters
.append(url
)
2320 if not len(season_posters
):
2322 graphics
= season_posters
2325 if self
.config
['nokeys'] and not self
.config
['download']:
2328 key_tag
=graphics_type
+u
':'
2331 wasanythingadded
= 0
2332 anyotherlanguagegraphics
=u
''
2333 englishlanguagegraphics
=u
''
2334 for URL
in graphics
:
2335 if graphics_type
== 'filename':
2336 if URL
[graphics_type
] == None:
2338 if lang
: # Is there a language to filter URLs on?
2339 if lang
== URL
['language']:
2340 if graphics_type
!= self
.ep_image_type
:
2341 graphicsURLs
+=key_tag
+URL
['_bannerpath']+'\n'
2343 graphicsURLs
+=key_tag
+URL
[graphics_type
]+'\n'
2344 else: # Check for fall back graphics in case there are no selected language graphics
2345 if u
'en' == URL
['language']:
2346 if graphics_type
!= self
.ep_image_type
:
2347 englishlanguagegraphics
+=key_tag
+URL
['_bannerpath']+'\n'
2349 englishlanguagegraphics
+=key_tag
+URL
[graphics_type
]+'\n'
2351 if graphics_type
!= self
.ep_image_type
:
2352 anyotherlanguagegraphics
+=key_tag
+URL
['_bannerpath']+'\n'
2354 anyotherlanguagegraphics
+=key_tag
+URL
[graphics_type
]+'\n'
2356 if graphics_type
!= self
.ep_image_type
:
2357 graphicsURLs
+=key_tag
+URL
['_bannerpath']+'\n'
2359 graphicsURLs
+=key_tag
+URL
[graphics_type
]+'\n'
2360 if wasanythingadded
== len(graphicsURLs
):
2362 wasanythingadded
= len(graphicsURLs
)
2364 if self
.config
['maximum']: # Has the maximum number of graphics been downloaded?
2365 if count
== int(self
.config
['maximum']):
2368 if not len(graphicsURLs
):
2369 if len(englishlanguagegraphics
): # Fall back to English graphics
2370 graphicsURLs
= englishlanguagegraphics
2371 elif len(anyotherlanguagegraphics
): # Fall back-back to any available graphics
2372 graphicsURLs
= anyotherlanguagegraphics
2374 if self
.config
['debug_enabled']:
2375 print "\nGraphics:\n", graphicsURLs
2377 if not len(graphicsURLs
): # Are there any graphics?
2380 if len(graphicsURLs
) == 1 and graphicsURLs
[0] == graphics_type
+':':
2381 return None # Due to the language filter there may not be any URLs
2383 return(graphicsURLs
)
2386 def getTopRatedGraphics(self
, graphics_type
):
2387 """Retrieve only the top rated series Poster, Fan Art and Banner graphics URL(s)
2388 return None if no top rated graphics URLs were found
2389 return a string of top rated URLs
2391 if graphics_type
== u
'filename':
2392 self
.config
['log'].debug(u
'! There are no such thing as top rated Episode image URLs')
2395 series_name
=self
.config
['series_name']
2396 keys
=self
.config
['nokeys']
2397 if self
._searchforSeries
(series_name
)[graphics_type
] != None:
2398 if keys
and not self
.config
['download']:
2399 toprated
=(self
._searchforSeries
(series_name
)[graphics_type
])+'\n'
2401 toprated
=(u
'%s:%s\n' % (graphics_type
, self
._searchforSeries
(series_name
)[graphics_type
]))
2403 # end getTopRatedGraphics
2405 def _downloadEpisodeData(self
,ep_data
):
2406 """Down load episode meta data and episode image graphics
2407 return True whether or not there was episode data processed
2409 if not len(ep_data
):
2410 return True # There were no episode data in the list
2411 ep_data_list
=[] # An array of episode meta data
2413 first_key
=self
.config
['ep_include_data'][0]+':'
2414 key_size
=len(first_key
)
2416 while len(ep_data
): # Grab each episode's set of meta data
2418 self
.config
['log'].debug(u
'Parse out the episode data from an episode meta dats string')
2419 end
= ep_data
[key_size
:].index(first_key
)
2420 ep_data_list
.append(ep_data
[:end
+key_size
])
2421 ep_data
=ep_data
[end
+key_size
:]
2423 ep_data_list
.append(ep_data
)
2426 if not self
.config
['metadatadir']:
2427 self
.config
['metadatadir'] = os
.getcwd()
2429 # Process each episode's meta data
2430 for episode
in ep_data_list
:
2431 tmp_data
= episode
.split('\n')
2432 for i
in range(len(tmp_data
)):
2433 tmp_data
[i
] = tmp_data
[i
].rstrip()# Remove \n characters from the end of each record
2435 for data
in tmp_data
:
2437 self
.config
['log'].debug(u
'Checking for key in episode meta data')
2438 tmp_dict
[data
[:data
.index(':')]] = data
[data
.index(':')+1:]
2441 tmp_dict
['ext']='meta'
2443 for key
in ['seasonnumber', 'episodenumber']:
2444 if tmp_dict
.has_key(key
):
2445 tmp_dict
[key
] = int(tmp_dict
[key
])
2446 if not tmp_dict
.has_key(u
'episodename'):
2447 tmp_dict
[u
'episodename'] = u
''
2448 filename
="%s/%s" % (self
.config
['metadatadir'],self
.config
['ep_metadata'] % tmp_dict
)
2449 image_filename
= None
2450 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2451 url
= tmp_dict
['filename']
2453 if not self
.config
['episodeimagedir']:
2454 self
.config
['episodeimagedir'] = self
.config
['allgraphicsdir']
2455 (dirName
, fileName
) = os
.path
.split(url
)
2456 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
2457 tmp_dict
[u
'ext']=fileExtension
[1:]
2458 image_filename
= "%s/%s" % (self
.config
['episodeimagedir'], self
.config
['ep_metadata'] % tmp_dict
)
2459 # Only download a file if it does not exist or the option overwrite is selected
2460 # or the option update is selected and the local meta data file is
2461 # older than the episode data on thetvdb.com wiki
2463 if self
.config
['update'] and tmp_dict
.has_key('lastupdated') and os
.path
.isfile(filename
):
2464 if int(tmp_dict
['lastupdated']) > int(os
.path
.getmtime(filename
)):
2467 if not self
.config
['overwrite'] and not outofdate
:
2468 if self
.config
['get_ep_meta'] and self
.config
['get_ep_image']:
2470 if os
.path
.isfile(filename
) and os
.path
.isfile(image_filename
):
2473 if os
.path
.isfile(filename
):
2475 elif self
.config
['get_ep_meta']:
2476 if os
.path
.isfile(filename
):
2478 elif self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2479 url
= tmp_dict
['filename']
2481 if os
.path
.isfile(image_filename
):
2488 if self
.config
['simulation']:
2489 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2490 self
.config
['log'].debug(u
'Simulate downloading an episode image')
2491 url
= tmp_dict
['filename']
2493 sys
.stdout
.write(u
"Simulation create episode image file(%s)\n" % image_filename
)
2494 if self
.config
['get_ep_meta']:
2496 u
"Simulation create meta data file(%s)\n" % filename
2500 if self
.config
['get_ep_image'] and tmp_dict
.has_key('filename'):
2501 if tmp_dict
['filename'] != 'None':
2502 self
._downloadGraphics
('filename:'+tmp_dict
['filename'])
2504 # Write out an episode meta data file
2505 if self
.config
['get_ep_meta']:
2506 fHandle
= codecs
.open(filename
, 'w', 'utf8')
2507 fHandle
.write(episode
)
2511 # end _downloadEpisodeData
2513 def _changeToCommas(self
,meta_data
):
2514 """Remove '|' and replace with commas
2515 return the modified text
2517 if not meta_data
: return meta_data
2518 meta_data
= (u
'|'.join([d
for d
in meta_data
.split('| ') if d
]))
2519 return (u
', '.join([d
for d
in meta_data
.split(u
'|') if d
]))
2520 # end _changeToCommas
2522 def _changeAmp(self
, text
):
2523 """Change & values to ASCII equivalents
2524 return the modified text
2526 if not text
: return text
2527 text
= text
.replace(""", u
"'").replace("\r\n", u
" ")
2528 text
= text
.replace(r
"\'", u
"'")
2532 def getSeriesEpisodeData(self
):
2533 """Get Series Episode meta data. This can be one specific episode or all of a seasons episodes
2534 or all episodes for an entire series.
2535 return an empy sting of no episode meta data was found
2536 reurn a string containing key value pairs of episode meta data
2538 sid
=self
.config
['sid']
2539 series_name
=self
.config
['series_name']
2540 season_num
=self
.config
['season_num']
2541 episode_num
=self
.config
['episode_num']
2542 episode_name
=self
.config
['episode_name']
2548 tmp_cast
= self
._searchforSeries
(series_name
)[u
'_actors']
2553 for cast
in tmp_cast
:
2554 cast_members
+=(cast
['name']+u
', ').encode('utf8')
2555 if cast_members
!= '':
2557 cast_members
= cast_members
[:-2].encode('utf8')
2558 except UnicodeDecodeError:
2559 cast_members
= unicode(cast_members
[:-2],'utf8')
2560 cast_members
= self
._changeAmp
(cast_members
)
2561 cast_members
= self
._changeToCommas
(cast_members
)
2562 cast_members
=cast_members
.replace('\n',' ')
2567 genres_string
= self
._searchforSeries
(series_name
)[u
'genre'].encode('utf8')
2570 if genres_string
!= None and genres_string
!= '':
2571 genres
= self
._changeAmp
(genres_string
)
2572 genres
= self
._changeToCommas
(genres
)
2574 seasons
=self
._searchforSeries
(series_name
).keys() # Get the seasons for this series
2575 episodes_metadata
=u
''
2576 for season
in seasons
:
2577 if season_num
: # If a season was specified skip other seasons
2578 if season
!= int(season_num
):
2580 episodes
=self
._searchforSeries
(series_name
)[season
].keys()# Get the episodes for this season
2581 for episode
in episodes
: # If an episode was specified skip other episodes
2583 if episode
!= int(episode_num
):
2586 if sid
: # Ouput the full series name
2588 ep_data
["series"]=self
._searchforSeries
(sid
)[u
'seriesname'].encode('utf8')
2589 except AttributeError:
2593 ep_data
["series"]=self
._searchforSeries
(series_name
)[u
'seriesname'].encode('utf8')
2594 except AttributeError:
2596 available_keys
=self
._searchforSeries
(series_name
)[season
][episode
].keys()
2598 ep_data
[u
'gueststars']=''
2599 for key
in available_keys
:
2600 if self
._searchforSeries
(series_name
)[season
][episode
][key
] == None:
2603 text
= self
._searchforSeries
(series_name
)[season
][episode
][key
]
2604 text
= self
._changeAmp
(text
)
2605 text
= self
._changeToCommas
(text
)
2606 ep_data
[key
.lower()]=text
.replace('\n',' ')
2607 for key
in self
.config
['ep_include_data']: # Select and sort the required meta data
2608 if ep_data
.has_key(key
):
2609 if key
== u
'gueststars':
2610 if ep_data
[key
] == '':
2611 tmp
+=u
'Cast:%s\n' % cast_members
2613 if (len(ep_data
[key
]) > 128) and not ep_data
[key
].count(','):
2614 tmp
+=u
'Cast:%s\n' % cast_members
2616 tmp
+=u
'Cast:%s, %s\n' % (cast_members
, ep_data
[key
])
2619 tmp
+=u
'%s:%s\n' % (key
, ep_data
[key
])
2620 except UnicodeDecodeError:
2621 tmp
+=u
'%s:%s\n' % (key
, unicode(ep_data
[key
], "utf8"))
2622 tmp
+=u
'Runtime:%s\n' % self
._searchforSeries
(series_name
)[u
'runtime']
2624 tmp
+=u
'Genres:%s\n' % genres
2626 episodes_metadata
+=tmp
2627 return episodes_metadata
2628 # end Getseries_episode_data
2630 def returnFilename(self
):
2631 """Return a single file name (excluding file extension and directory), limited by the current
2632 variables (sid, season name, season number ... etc). Typically used when writing a meta file
2633 or naming/renaming a video file after a TV show recording.
2634 return False and out put an error if there not either a series id (SID) or series name
2635 return False and out put an error if there proper episode information (numbers or name)
2636 return False if the option (-MGF) used and there is not exact TV series name match
2637 return a specific episode filename
2639 sid
=self
.config
['sid']
2640 series_name
=self
.config
['series_name']
2641 season_num
=self
.config
['season_num']
2642 episode_num
=self
.config
['episode_num']
2643 episode_name
=self
.config
['episode_name']
2645 if not sid
and not series_name
:
2647 u
"\n! Warning: There must be at least series name or SID to request a filename\n"
2651 if season_num
and episode_num
:
2653 elif not episode_name
:
2655 u
'\n! Error: There must be at least "season and episode numbers" or "episode name" to request a filename\n'
2659 # Special logic must be used if the (-MG) guessing option has been requested
2660 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
2662 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
2663 except Exception, e
:
2664 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
))
2666 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
, self
.config
['series_name'].lower()):
2667 self
.config
['sid'] = allmatchingseries
['sid']
2668 self
.config
['series_name'] = allmatchingseries
['name']
2670 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'])
2673 episode
= self
.verifySeriesExists()
2675 if not episode
: # Make sure an episode was found
2677 u
'\n! Error: The episode was not found for series(%s), Episode name(%s)\n' % (series_name
, episode_name
)
2681 sid
=self
.config
['sid']
2683 if UI_selectedtitle
and (self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']):
2684 self
.config
['series_name'] = UI_selectedtitle
2686 series_name
=self
.config
['series_name']
2687 season_num
=self
.config
['season_num']
2688 episode_num
=self
.config
['episode_num']
2689 episode_name
=self
.config
['episode_name']
2691 tmp_dict
={'series': series_name, 'seasonnumber': season_num, 'episodenumber': episode_num, 'episodename': episode_name, 'sid': sid }
2694 for key
in ['seasonnumber', 'episodenumber']:
2695 if tmp_dict
.has_key(key
):
2696 tmp_dict
[key
] = int(tmp_dict
[key
])
2698 return self
.sanitiseFileName(u
"%s" % (self
.config
['ep_metadata'] % tmp_dict
)[:-1])
2699 # end returnFilename
2701 def processTVdatabaseRequests(self
):
2702 """Process the data/download requests as indicated by the variables
2703 return None if the series/season/episode does not exist
2704 return None if there is no data to process for the request actions
2705 return a string for display or further processing that satisfies the reqested actions
2707 if self
.verifySeriesExists():# Getting a filename is a single event nothing else is returned
2708 if self
.config
['ret_filename']:
2709 return self
.returnFilename()
2713 types
={'get_fanart': self.fanart_type, 'get_poster': self.poster_type, 'get_banner': self.banner_type}
2714 if self
.config
['toprated']:
2715 typegetGraphics
=self
.getTopRatedGraphics
2717 typegetGraphics
=self
.getGraphics
2719 if self
.verifySeriesExists():
2720 if self
.config
['download']: # Deal only with graphics display or downloads
2721 for key
in types
.keys():
2722 if key
== 'get_ep_image': # Ep image downloads processed below
2724 if self
.config
[key
]:
2725 if self
._downloadGraphics
(typegetGraphics(types
[key
])):
2727 u
"%s downloading successfully processed\n" % key
.title()
2731 for key
in types
.keys():
2732 if self
.config
[key
]:
2733 string
=typegetGraphics(types
[key
])
2736 if url_string
!= '':
2737 results
+=url_string
# Add graphic URLs to returned results
2739 # Should episode meta data or episode image be processed?
2740 if self
.config
['get_ep_meta'] or self
.config
['get_ep_image']:
2741 if self
.config
['download']: # Deal only with episode data display or download
2742 if self
._downloadEpisodeData
(self
.getSeriesEpisodeData()):
2744 u
"Episode meta data and/or images downloads successfully processed\n"
2747 eps_string
= self
.getSeriesEpisodeData()
2748 if eps_string
!= '':
2749 results
+=eps_string
# Add episode meta data to returned results
2754 if results
[len(results
)-1] == '\n':
2755 return results
[:len(results
)-1]
2760 # end processTVdatabaseRequests
2762 def __repr__(self
): # Just a place holder
2769 class VideoFiles(Tvdatabase
):
2770 """Process all video file and/or directories containing video files. These TV Series video
2771 files must be named so that a "series name or sid" and/or "season and episode number"
2772 can be extracted from the video file name. It is best to have renamed the TV series video files with
2773 tvnamer before using these files with jamu. Any video file without season and episode numbers is
2774 assumed to be a movie. Files that do not match the previously described criterion will be skipped.
2775 tvnamer can be found at:
2776 http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search
2778 def __init__(self
, configuration
):
2779 """Retrieve the configuration options
2781 super(VideoFiles
, self
).__init
__(configuration
)
2784 image_extensions
= ["png", "jpg", "bmp"]
2786 def _findFiles(self
, args
, recursive
= False, verbose
= False):
2788 Takes a file name or folder path and grabs files inside them. Does not recurse
2789 more than one level (if a folder is supplied, it will list files within),
2790 unless recurse is True, in which case it will recursively find all files.
2791 return an array of file names
2795 for cfile
in args
: # Directories must exist and be both readable and writable
2796 if os
.path
.isdir(cfile
) and not os
.access(cfile
, os
.F_OK | os
.R_OK
):
2797 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
))
2800 if os
.path
.isdir(cfile
):
2801 for directory
in self
.config
['ignore-directory']: # ignore directory list
2802 if not cfile
.startswith(directory
):
2805 if ignore
: # Skip this directory
2807 if os
.path
.isdir(cfile
):
2808 index
= cfile
.find(u
'VIDEO_TS')
2810 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
))
2813 cfile
= unicode(cfile
, u
'utf8')
2814 except (UnicodeEncodeError, TypeError):
2816 for sf
in os
.listdir(cfile
):
2818 newpath
= os
.path
.join(cfile
, sf
)
2820 sys
.stderr
.write(u
"\n! Error: This video file cannot be processed skipping:\n")
2821 sys
.stderr
.write(sf
)
2822 sys
.stderr
.write(u
"\nIt may be advisable to rename this file and try again.\n\n")
2824 if os
.path
.isfile(newpath
):
2825 allfiles
.append(newpath
)
2829 self
._findFiles
([newpath
], recursive
= recursive
, verbose
= verbose
)
2834 elif self
.config
[u
'file_move_flag'] and not os
.access(cfile
, os
.F_OK | os
.R_OK | os
.W_OK
):
2835 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
))
2836 elif os
.path
.isfile(cfile
) and os
.access(cfile
, os
.F_OK | os
.R_OK
):
2837 allfiles
.append(cfile
) # Files must exist and be at least readable
2844 def _processNames(self
, names
, verbose
=False, movies
=False):
2846 Takes list of names, runs them though the self.config['name_parse'] regex parsing strings
2847 to extract series name, season and episode numbers. Non-video files are skipped.
2848 return an array of dictionaries containing series name, season and episode numbers, file path and full filename and file extention.
2852 filepath
, filename
= os
.path
.split( f
)
2853 filename
, ext
= os
.path
.splitext( filename
)
2855 # Remove leading . from extension
2856 ext
= ext
.replace(u
".", u
"", 1)
2857 self
.config
['log'].debug(u
'Checking for a valid video filename extension')
2858 if not ext
.lower() in self
.config
[u
'video_file_exts']:
2859 for key
in self
.image_extensions
:
2863 sys
.stderr
.write(u
"\n! Warning: Skipping non-video file name: (%s)\n" % (f
))
2867 for r
in self
.config
['name_parse']:
2868 match
= r
.match(filename
)
2870 # If the filename does not match the default regular
2871 # expressions, try to match the file path + filename with the
2872 # extended fullpath regular expression so we can extract the
2873 # needed information out of the pathname
2875 for r
in self
.config
['fullname_parse']:
2876 match
= r
.match(os
.path
.join(filepath
, filename
))
2881 self
.config
['log'].debug(u
'matched reg:%s'%match
.re
.pattern
)
2882 seriesname
, seasno
, epno
= match
.groups()
2884 #remove ._- characters from name (- removed only if next to end of line)
2885 seriesname
= re
.sub("[\._]|\-(?=$)", " ", seriesname
).strip()
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()
2907 allEps
.append({ 'file_seriesname':movie
,
2910 'filepath':filepath
,
2911 'filename':filename
,
2913 'categories': categories
2915 except UnicodeDecodeError:
2916 allEps
.append({ 'file_seriesname':unicode(movie
,'utf8'),
2919 'filepath':unicode(filepath
,'utf8'),
2920 'filename':unicode(filename
,'utf8'),
2921 'ext':unicode(ext
,'utf8'),
2922 'categories': categories
2925 categories
+=u
', TV Series'
2927 allEps
.append({ 'file_seriesname':seriesname
,
2930 'filepath':filepath
,
2931 'filename':filename
,
2933 'categories': categories
2935 except UnicodeDecodeError:
2936 allEps
.append({ 'file_seriesname':unicode(seriesname
,'utf8'),
2939 'filepath':unicode(filepath
,'utf8'),
2940 'filename':unicode(filename
,'utf8'),
2941 'ext':unicode(ext
,'utf8'),
2942 'categories': categories
2945 if movies
: # Account for " - On DVD" and " HD - On DVD" extra text on file names
2946 categories
+=u
', Movie'
2949 if movie
.endswith(self
.config
['hd_dvd']):
2950 movie
= movie
.replace(self
.config
['hd_dvd'], '')
2951 categories
+=u
', DVD'
2954 if movie
.endswith(self
.config
['dvd']):
2955 movie
= movie
.replace(self
.config
['dvd'], '')
2956 categories
+=u
', DVD'
2957 movie
= re
.sub("[\._]|\-(?=$)", " ", movie
).strip()
2959 allEps
.append({ 'file_seriesname':movie
,
2962 'filepath':filepath
,
2963 'filename':filename
,
2965 'categories': categories
2967 except UnicodeDecodeError:
2968 allEps
.append({ 'file_seriesname':unicode(movie
,'utf8'),
2971 'filepath':unicode(filepath
,'utf8'),
2972 'filename':unicode(filename
,'utf8'),
2973 'ext':unicode(ext
,'utf8'),
2974 'categories': categories
2977 sys
.stderr
.write(u
"\n! Warning: Skipping invalid name: %s\n" % (f
))
2985 def processFileOrDirectory(self
):
2986 '''This routine is NOT used for MythTV meta data processing.
2987 If directory path has been specified then create a list of files that qualify as video
2988 files / including recursed directories.
2989 Then parse the list of file names to determine (series, season number, ep number and ep name).
2990 Skip any video file that cannot be parsed for sufficient info.
2991 Loop through the list:
2992 > Check if the series, season, ... exists, skip with debug message if none found
2993 > Set variable with proper info: sid, series, season and episode numbers
2994 > Process the file's information per the variable to get graphics and or meta data
2995 return False and an error message and exist the script if there are no video files to process
2996 return None when all processing was complete
2997 return a string of file names if the "Filename" process option was True
3000 allFiles
= self
._findFiles
(self
.config
['video_dir'], self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
3001 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'])
3003 if len(validFiles
) == 0:
3004 sys
.stderr
.write(u
"\n! Error: No valid video files found\n")
3007 path_flag
= self
.config
['metadatadir']
3008 for cfile
in validFiles
:
3009 sys
.stdout
.write(u
"# Processing %(file_seriesname)s (season: %(seasno)d, episode %(epno)d)\n" % (cfile
))
3010 self
.config
['sid']=None
3011 self
.config
['episode_name'] = None
3012 self
.config
['series_name']=cfile
['file_seriesname']
3013 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3014 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3015 if not path_flag
: # If no metaddata directory specified then default to the video file dir
3016 self
.config
['metadatadir'] = cfile
['filepath']
3017 if self
.verifySeriesExists():
3018 self
.config
['log'].debug(u
"Found series(%s) season(%s) episode(%s)" % (self
.config
['series_name'], self
.config
['season_num'], self
.config
['episode_num']))
3019 if self
.config
['ret_filename']:
3020 returned
= self
.processTVdatabaseRequests()
3021 if returned
!= None and returned
!= False:
3022 filenames
+=returned
+'\n'
3024 self
.processTVdatabaseRequests()
3026 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']))
3027 self
.config
['log'].debug("# Done")
3028 if len(filenames
) == 0:
3031 return filenames
[:-1] # drop the last '\n'
3032 # end processFileOrDirectory
3034 def __repr__(self
): # Just a place holder
3041 class MythTvMetaData(VideoFiles
):
3042 """Process all mythvideo video files, update the video files associated MythTV meta data.
3043 Download graphics for those video files from either thetvdb.com or themovie.com. Video file names
3044 for TV episodes must series name, season and episode numbers. The video file's movie name must be
3045 an exact match with a movie title in themoviedb.com or the MythTV database must have an entry for
3046 the video file with a TMDB or an IMDB number (db field 'intref').
3048 def __init__(self
, configuration
):
3049 """Retrieve the configuration options
3051 super(MythTvMetaData
, self
).__init
__(configuration
)
3055 # A dictionary of meta data keys and initialized values
3056 global graphicsDirectories
3057 movie_file_format
=u
"%s/%s.%s"
3058 graphic_suffix
= {u'coverfile': u'_coverart', u'fanart': u'_fanart', u'banner': u'_banner'}
3059 graphic_name_suffix
= u
"%s/%s%s.%s"
3060 graphic_name_season_suffix
= u
"%s/%s Season %d%s.%s"
3063 def _getSubtitle(self
, cfile
):
3064 '''Get the MythTV subtitle (episode name)
3066 return episode name string
3068 self
.config
['sid']=None
3069 self
.config
['episode_name'] = None
3070 self
.config
['series_name']=cfile
['file_seriesname']
3071 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3072 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3073 self
.verifySeriesExists()
3074 return self
.config
['episode_name']
3078 def hashFile(self
, name
):
3079 '''Create metadata hash values for mythvideo files
3081 return u'' if the was an error with the video file or the video file length was zero bytes
3083 filename
= self
.rtnRelativePath(name
, u
'mythvideo')
3084 # Use the MythVideo hashing protocol when the video is in a storage groups
3085 if filename
[0] != u
'/':
3086 hash_value
= FileOps(mythbeconn
.hostname
).getHash(filename
, u
'Videos')
3087 if hash_value
== u
'NULL':
3092 # Use a local hashing routine when video is not in a Videos storage group
3093 # For original code: http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#Python
3095 longlongformat
= 'q' # long long
3096 bytesize
= struct
.calcsize(longlongformat
)
3097 f
= open(name
, "rb")
3098 filesize
= os
.path
.getsize(name
)
3100 if filesize
< 65536 * 2: # Video file is too small
3102 for x
in range(65536/bytesize
):
3103 buffer = f
.read(bytesize
)
3104 (l_value
,)= struct
.unpack(longlongformat
, buffer)
3106 hash = hash & 0xFFFFFFFFFFFFFFFF #to remain as 64bit number
3107 f
.seek(max(0,filesize
-65536),0)
3108 for x
in range(65536/bytesize
):
3109 buffer = f
.read(bytesize
)
3110 (l_value
,)= struct
.unpack(longlongformat
, buffer)
3112 hash = hash & 0xFFFFFFFFFFFFFFFF
3114 returnedhash
= "%016x" % hash
3117 except(IOError): # Accessing to this video file caused and error
3121 def rtnRelativePath(self
, abpath
, filetype
):
3122 '''Check if there is a Storage Group for the file type (video, coverfile, banner, fanart, screenshot)
3123 and form an apprioriate relative path and file name.
3124 return a relative path and file name
3125 return an absolute path and file name if there is no storage group for the file type
3130 # There is a chance that this is already a relative path or there is no Storage group for file type
3131 if not len(storagegroups
):
3133 if not storagegroups
.has_key(filetype
) or abpath
[0] != '/':
3136 # The file must already be in one of the directories specified by the file type's storage group
3137 for directory
in storagegroups
[filetype
]:
3138 if abpath
.startswith(directory
):
3139 return abpath
[len(directory
)+1:]
3142 # end rtnRelativePath
3144 def rtnAbsolutePath(self
, relpath
, filetype
):
3145 '''Check if there is a Storage Group for the file type (mythvideo, coverfile, banner, fanart,
3146 screenshot) and form an appropriate absolute path and file name.
3147 return an absolute path and file name
3148 return the relpath sting if the file does not actually exist in the absolute path location
3150 if relpath
== None or relpath
== u
'':
3153 # There is a chance that this is already an absolute path
3154 if relpath
[0] == u
'/':
3157 if self
.absolutepath
:
3158 if not len(self
.config
['localpaths'][filetype
]):
3160 directories
= self
.config
['localpaths'][filetype
]
3162 directories
= self
.config
[filetype
]
3164 for directory
in directories
:
3165 abpath
= u
"%s/%s" % (directory
, relpath
)
3166 if os
.path
.isfile(abpath
): # The file must actually exist locally
3169 return relpath
# The relative path does not exist at all the metadata entry is useless
3170 # end rtnAbsolutePath
3173 def removeCommonWords(self
, title
):
3174 '''Remove common words from a title
3175 return title striped of common words
3179 wordList
= [u
'the ', u
'a ', u
' '] # common word list. Leave double space as the last value.
3180 title
= title
.lower()
3181 for word
in wordList
:
3182 title
= title
.replace(word
, u
'')
3185 return filter(is_not_punct_char
, title
.strip())
3186 # end removeCommonWords()
3189 def _getTmdbIMDB(self
, title
, watched
=False, IMDB
=False, rtnyear
=False):
3190 '''Find and exact match of the movie name with what's on themoviedb.com
3191 If IMDB is True return an imdb#
3192 If rtnyear is True return IMDB# and the movie year in a dictionary
3193 return False (no matching movie found)
3194 return imdb# and/or tmdb#
3196 global video_type
, UI_title
3197 UI_title
= title
.replace(self
.config
[u
'hd_dvd'], u
'')
3198 UI_title
= UI_title
.replace(self
.config
[u
'dvd'], u
'')
3200 if UI_title
[-1:] == ')': # Get rid of the (XXXX) year from the movie title
3201 tmp_title
= UI_title
[:-7].lower()
3203 tmp_title
= UI_title
.lower()
3205 if self
.config
['series_name_override']:
3206 if self
.config
['series_name_override'].has_key(tmp_title
):
3207 return (self
.config
['series_name_override'][tmp_title
]).strip()
3216 results
= [self
.config
['tmdb_api'].searchIMDB(IMDB
)]
3218 results
= self
.config
['tmdb_api'].searchTMDB(user_tmdb
)
3220 if results
.has_key('releasedate'):
3221 return {'name': "%s (%s)" % (results['title'], results['releasedate'][:4]), u'sid': results[u'inetref']}
3223 return {'name': "%s" % (results['title'], ), u'sid': results[u'inetref']}
3225 return results
['inetref']
3227 results
= self
.config
['tmdb_api'].searchTitle(tmp_title
)
3228 except TmdbMovieOrPersonNotFound
, e
:
3230 except Exception, errormsg
:
3231 self
._displayMessage
(u
"themoviedb.com error for Movie(%s) invalid data error (%s)" % (title
, errormsg
))
3234 self
._displayMessage
(u
"themoviedb.com error for Movie(%s)" % title
)
3237 # Check if user's interactive response (Skip, selection, input #)
3238 if len(results
[0]) and self
.config
['interactive']:
3239 if results
[0].has_key('userResponse'):
3240 # Check if the user selected a specific movie from the list
3241 if results
[0]['userResponse'] == 'User selected':
3243 if results
[0].has_key('released'):
3244 data
= {'name': "%s (%s)" % (results[0]['name'], results[0]['released'][:4]), u'sid': results[0][u'id']}
3246 data
= {'name': "%s" % (results[0]['name'], ), u'sid': results[0][u'id']}
3249 return results
[0]['id']
3250 # Check if the user has entered a TMDB number themselves
3251 if results
[0]['userResponse'] == 'User input':
3252 user_tmdb
= results
[0]['id']
3254 # Check if the user wants this video to be ignored by Jamu from now on
3255 if results
[0]['id'] == '99999999':
3259 return results
[0]['id']
3262 if IMDB
: # This is required to allow graphic file searching both by a TMDB and IMDB numbers
3264 if results
[0].has_key('imdb_id'):
3265 return results
[0]['imdb_id'][2:]
3271 if UI_title
[-1:] == ')':
3272 name
= UI_title
[:-7].lower() # Just the movie title
3273 year
= UI_title
[-5:-1] # The movie release year
3275 name
= tmp_title
.lower()
3277 name
= name
.strip().replace(' ', ' ')
3280 for movie
in results
:
3281 if self
.removeCommonWords(movie
['name']) == self
.removeCommonWords(name
):
3283 if movie
.has_key('released'):
3284 TMDB_movies
.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3286 TMDB_movies
.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}
)
3288 if movie
.has_key(u
'released'):
3289 if movie
['released'][:4] == year
:
3291 return {'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
3294 TMDB_movies
.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3297 TMDB_movies
.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']}
)
3299 elif movie
.has_key('alternative_name'):
3300 if self
.removeCommonWords(movie
['alternative_name']) == self
.removeCommonWords(name
):
3302 if movie
.has_key('released'):
3303 TMDB_movies
.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3305 TMDB_movies
.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}
)
3307 if movie
.has_key(u
'released'):
3308 if movie
['released'][:4] == year
:
3310 return {'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
3313 TMDB_movies
.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
)
3316 TMDB_movies
.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']}
)
3319 # When there is only one match but NO year to confirm then it is OK to assume an exact match
3320 if len(TMDB_movies
) == 1 and year
== '':
3322 return TMDB_movies
[0]
3324 return TMDB_movies
[0][u
'sid']
3326 if imdb_lib
: # Can a imdb.com search be done?
3327 imdb_access
= imdb
.IMDb()
3330 movies_found
= imdb_access
.search_movie(tmp_title
.encode("ascii", 'ignore'))
3333 if not len(movies_found
):
3336 for movie
in movies_found
: # Get rid of duplicates
3337 try: # Protect against bad data from IMDBpy
3338 if movie
.has_key('year'):
3339 temp
= {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])}
3341 temp
= {imdb_access.get_imdbID(movie): movie['title']}
3344 if tmp_movies
.has_key(temp
.keys()[0]):
3346 tmp_movies
[temp
.keys()[0]] = temp
[temp
.keys()[0]]
3347 for movie
in tmp_movies
:
3348 if tmp_movies
[movie
][:-7].lower() == name
or self
.removeCommonWords(tmp_movies
[movie
][:-7]) == self
.removeCommonWords(name
):
3350 if tmp_movies
[movie
][-5:-1] == year
:
3352 return {'name': tmp_movies[movie], u'sid': movie}
3354 return u
"%07d" % int(movie
) # Pad out IMDB# with leading zeroes
3355 IMDB_movies
.append({'name': tmp_movies[movie], u'sid': movie}
)
3357 if len(IMDB_movies
) == 1: # If this is the only choice and titles matched then auto pick it
3358 if self
.removeCommonWords(IMDB_movies
[0]['name'][:-7]) == self
.removeCommonWords(name
):
3360 return IMDB_movies
[0]
3362 return u
"%07d" % int(IMDB_movies
[0][u
'sid'])
3364 # Does IMDB list this movie?
3365 if len(IMDB_movies
) == 0:
3368 # Did the user want an interactive interface?
3369 if not self
.config
['interactive']:
3372 # Force only an IMDB look up for a movie
3373 movies
= IMDB_movies
3376 ui
= jamu_ConsoleUI(config
= self
.config
, log
= self
.config
['log'])
3378 inetref
= ui
.selectSeries(movies
)
3379 except tvdb_userabort
:
3380 if video_type
==u
'IMDB' or len(IMDB_movies
) == 0:
3381 self
._displayMessage
(u
"1-No selection made for Movie(%s)" % title
)
3383 movies
= IMDB_movies
3386 inetref
= ui
.selectSeries(movies
)
3387 except tvdb_userabort
:
3388 self
._displayMessage
(u
"2-No selection made for Movie(%s)" % title
)
3391 if inetref
.has_key('sid'):
3392 if _can_int(inetref
['sid']):
3393 if inetref
['sid'] == '99999999':
3394 return inetref
['sid']
3396 if inetref
['name'] == u
'User input':
3398 data
= imdb_access
.get_movie(inetref
['sid'])
3399 if data
.has_key('long imdb title'):
3400 return {'name': data['long imdb title'], u'sid': inetref['sid']}
3401 elif data
.has_key('title'):
3402 return {'name': data['title'], u'sid': inetref['sid']}
3405 except imdb
._exceptions
.IMDbDataAccessError
:
3410 return u
"%07d" % int(inetref
['sid']) # Pad out IMDB# with leading zeroes
3417 def _getTmdbGraphics(self
, cfile
, graphic_type
, watched
=False):
3418 '''Download either a movie Poster or Fanart
3420 return full qualified path and filename of downloaded graphic
3422 if graphic_type
== u
'-P':
3423 graphic_name
= u
'poster'
3424 key_type
= u
'coverart'
3425 rel_type
= u
'coverfile'
3427 graphic_name
= u
'fanart'
3428 key_type
= u
'fanart'
3431 self
.config
['series_name']=cfile
['file_seriesname']
3433 if len(cfile
['inetref']) == 7: # IMDB number
3434 results
= self
.config
['tmdb_api'].searchIMDB(cfile
['inetref'])
3436 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3437 except TmdbMovieOrPersonNotFound
, e
:
3438 self
._displayMessage
(u
"0-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3440 except Exception, e
:
3441 self
._displayMessage
(u
"themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_name
, e
))
3445 if not results
.has_key(key_type
):
3446 self
._displayMessage
(u
"1-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3449 self
._displayMessage
(u
"1b-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3452 graphic_file
= (results
[key_type
].split(u
','))[0].strip() # Only want the first image URL
3454 self
.config
['g_defaultname']=False
3455 self
.config
['toprated'] = True
3456 self
.config
['nokeys'] = False
3458 self
.config
['sid']=None
3460 if self
.program_seriesid
== None:
3461 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season 1'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3463 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3465 self
.config
['g_series'] = cfile
['inetref']+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3466 if graphic_type
== '-P':
3471 self
.config
['season_num']= None # Needed to get graphics named in 'g_series' format
3473 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3475 tmp_URL
= graphic_file
.replace(u
"http://", u
"")
3476 graphic_file
= u
"http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
3477 value
= self
._downloadGraphics
(u
"%s:%s" % (g_type
, graphic_file
), mythtv
=True)
3479 self
.config
['overwrite'] = False # Turn off overwriting
3482 self
._displayMessage
(u
"2-tmdb %s for Movie not found(%s)(%s)" % (graphic_name
, cfile
['filename'], cfile
['inetref']))
3485 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3486 # end _getTmdbGraphics
3488 def _getSecondarySourceGraphics(self
, cfile
, graphic_type
, watched
=False):
3489 '''Download from secondary source such as movieposter.com
3491 return full qualified path and filename of downloaded graphic
3493 if not len(self
.config
['myth_secondary_sources']):
3496 if graphic_type
== u
'coverfile':
3497 graphic_type
= u
'poster'
3498 rel_type
= u
'coverfile'
3500 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
3501 if not self
.config
['myth_secondary_sources'].has_key('movies'):
3503 if self
.config
['myth_secondary_sources']['movies'].has_key(graphic_type
):
3504 source
= self
.config
['myth_secondary_sources']['movies'][graphic_type
]
3505 if source
.find(u
'%(imdb)s') != -1:
3506 if len(cfile
['inetref']) != 7:
3508 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3509 except TmdbMovieOrPersonNotFound
, e
:
3510 self
._displayMessage
(u
"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_type
, e
))
3512 except Exception, e
:
3513 self
._displayMessage
(u
"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile
['file_seriesname'], graphic_type
, e
))
3517 if not results
.has_key('imdb'):
3518 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']))
3520 cfile
['imdb'] = results
['imdb']
3522 cfile
['imdb'] = cfile
['inetref']
3526 if not self
.config
['myth_secondary_sources'].has_key('tv'):
3528 if self
.config
['myth_secondary_sources']['tv'].has_key(graphic_type
):
3529 source
= self
.config
['myth_secondary_sources']['tv'][graphic_type
]
3533 self
.config
['series_name']=cfile
['file_seriesname']
3535 if self
.config
['simulation']:
3536 sys
.stdout
.write(u
"Simulating - downloading Secondary Source graphic (%s)\n" % cfile
['file_seriesname'])
3537 return u
"Simulated Secondary Source graphic filename place holder"
3539 # Test that the secondary's required data has been passed
3541 command
= source
% cfile
3543 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
))
3546 tmp_files
= callCommandLine(command
)
3548 self
._displayMessage
(u
"\n! Warning: Source (%s)\n could not find (%s) for (%s)(%s)\n" % (source
% cfile
, graphic_type
, cfile
['filename'], cfile
['inetref']))
3551 tmp_array
=tmp_files
.split('\n')
3552 if tmp_array
[0].startswith(u
'Failed'):
3553 self
._displayMessage
(u
"\n! Warning: Source (%s)\nfailed to download (%s) for (%s)(%s)\n" % (source
% cfile
, graphic_type
, cfile
['filename'], cfile
['inetref']))
3556 if tmp_array
[0].startswith(u
'file://'):
3557 tmp_files
=tmp_array
[0].replace(u
'file://', u
'')
3558 if not os
.path
.isfile(tmp_files
):
3559 sys
.stderr
.write(u
'\n! Error: The graphic file does not exist (%s)\n' % tmp_files
)
3562 # Fix file extentions in all caps or 4 character JPEG extentions
3563 fileExtension
= (_getExtention(tmp_files
)).lower()
3564 if fileExtension
== u
'jpeg':
3565 fileExtension
= u
'jpg'
3567 if self
.program_seriesid
== None:
3568 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], self
.sanitiseFileName(cfile
['file_seriesname']), self
.graphic_suffix
[rel_type
], fileExtension
)
3570 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], self
.sanitiseFileName(self
.program_seriesid
), self
.graphic_suffix
[rel_type
], fileExtension
)
3572 filename
= u
'%s/%s%s.%s' % (self
.config
['posterdir'][0], cfile
['inetref'], self
.graphic_suffix
[rel_type
], fileExtension
)
3574 if os
.path
.isfile(filename
): # This may be the same small file or worse then current
3576 (width
, height
) = self
.config
['image_library'].open(filename
).size
3577 (width2
, height2
) = self
.config
['image_library'].open(tmp_files
).size
3579 os
.remove(tmp_files
)
3584 # Verify that the downloaded file was NOT HTML instead of the intended file
3585 if self
._checkValidGraphicFile
(tmp_files
, graphicstype
=u
'', vidintid
=False) == False:
3586 os
.remove(tmp_files
) # Delete the useless HTML text
3588 shutil
.copy2(tmp_files
, filename
)
3589 os
.remove(tmp_files
)
3590 self
.num_secondary_source_graphics_downloaded
+=1
3591 return self
.rtnRelativePath(filename
, graphicsDirectories
[rel_type
])
3593 graphic_file
= tmp_array
[0]
3595 self
.config
['g_defaultname']=False
3596 self
.config
['toprated'] = True
3597 self
.config
['nokeys'] = False
3599 self
.config
['sid']=None
3601 if self
.program_seriesid
== None:
3602 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3604 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3606 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['inetref'])+self
.graphic_suffix
[rel_type
]+'.%(ext)s'
3607 g_type
= graphic_type
3609 self
.config
['season_num']= None # Needed to get graphics named in 'g_series' format
3611 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3613 tmp_URL
= graphic_file
.replace(u
"http://", u
"")
3614 graphic_file
= u
"http://"+urllib
.quote(tmp_URL
.encode("utf-8"))
3615 value
= self
._downloadGraphics
(u
"%s:%s" % (g_type
, graphic_file
), mythtv
=True)
3617 self
.config
['overwrite'] = False # Turn off overwriting
3619 self
._displayMessage
(u
"Secondary source %s not found(%s)(%s)" % (graphic_file
, cfile
['filename'], cfile
['inetref']))
3622 self
.num_secondary_source_graphics_downloaded
+=1
3623 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3624 # end _getSecondarySourceGraphics
3626 def combineMetaData(self
, available_metadata
, meta_dict
, vid_type
=False):
3627 ''' Combine the current data with new meta data from primary or secondary sources
3628 return combinted meta data dictionary
3631 for key
in meta_dict
.keys():
3632 if key
in self
.config
['metadata_exclude_as_update_trigger']:
3635 if key
== 'inetref' and available_metadata
[key
] != meta_dict
[key
]:
3636 available_metadata
[key
] = meta_dict
[key
]
3638 if key
== 'releasedate' and available_metadata
[key
] != meta_dict
[key
]:
3639 available_metadata
[key
] = meta_dict
[key
]
3641 if key
== 'userrating' and available_metadata
[key
] == 0.0:
3642 available_metadata
[key
] = meta_dict
[key
]
3644 if key
== 'length' and available_metadata
[key
] == 0:
3645 available_metadata
[key
] = meta_dict
[key
]
3647 if key
== 'rating' and (available_metadata
[key
] == 'NR' or available_metadata
[key
] == 'Unknown'):
3648 available_metadata
[key
] = meta_dict
[key
]
3650 if key
== 'year' and available_metadata
[key
] == 1895:
3651 available_metadata
[key
] = meta_dict
[key
]
3653 if key
== 'category' and available_metadata
[key
] == 0:
3654 available_metadata
[key
] = meta_dict
[key
]
3656 if key
== 'inetref' and available_metadata
[key
] == '00000000':
3657 available_metadata
[key
] = meta_dict
[key
]
3660 available_metadata
[key
] = meta_dict
[key
]
3662 if vid_type
and key
== 'subtitle': # There are no subtitles in movies
3664 if key
== 'plot': # Remove any line-feeds from the plot. Mythvideo does not expect them.
3665 meta_dict
[key
] = meta_dict
[key
].replace('\n', ' ')
3666 if (vid_type
and key
== 'plot') and (meta_dict
[key
].find('@') != -1 or len(meta_dict
[key
].split(' ')) < 10):
3668 if vid_type
and key
== 'plot':
3669 if available_metadata
[key
] != None:
3670 if len(available_metadata
[key
].split(' ')) < 10 and len(meta_dict
[key
].split(' ')) > 10:
3671 available_metadata
[key
] = meta_dict
[key
]
3673 if not available_metadata
.has_key(key
): # Mainly for Genre, Cast and Countries
3674 available_metadata
[key
] = meta_dict
[key
]
3676 if available_metadata
[key
] == None or available_metadata
[key
] == '' or available_metadata
[key
] == 'None' or available_metadata
[key
] == 'Unknown':
3677 available_metadata
[key
] = meta_dict
[key
]
3679 return available_metadata
3680 # end combineMetaData
3683 def _getSecondarySourceMetadata(self
, cfile
, available_metadata
):
3684 '''Download meta data from secondary source
3685 return available_metadata (returns the current metadata unaltered)
3686 return dictionary of combined meta data
3688 if not len(self
.config
['myth_secondary_sources']):
3689 return available_metadata
3691 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
3692 if not self
.config
['myth_secondary_sources'].has_key('movies'):
3693 return available_metadata
3695 if self
.config
['myth_secondary_sources']['movies'].has_key('metadata'):
3696 source
= self
.config
['myth_secondary_sources']['movies']['metadata']
3697 if source
.find(u
'%(imdb)s') != -1:
3698 if len(cfile
['inetref']) != 7:
3700 results
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3701 except TmdbMovieOrPersonNotFound
, e
:
3702 self
._displayMessage
(u
"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile
['file_seriesname'], e
))
3703 return available_metadata
3704 except Exception, e
:
3705 self
._displayMessage
(u
"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile
['file_seriesname'], e
))
3706 return available_metadata
3708 return available_metadata
3709 if not results
.has_key('imdb'):
3710 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']))
3711 return available_metadata
3712 cfile
['imdb'] = results
['imdb']
3714 cfile
['imdb'] = cfile
['inetref']
3716 return available_metadata
3718 if not self
.config
['myth_secondary_sources'].has_key('tv'):
3719 return available_metadata
3721 if self
.config
['myth_secondary_sources']['tv'].has_key('metadata'):
3722 source
= self
.config
['myth_secondary_sources']['tv']['metadata']
3724 return available_metadata
3726 # Test that the secondary's required data has been passed
3728 command
= source
% cfile
3730 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
))
3731 return available_metadata
3733 self
.config
['series_name']=cfile
['file_seriesname']
3736 tmp_files
= (callCommandLine(command
)).decode("utf8")
3738 self
._displayMessage
(u
"1-Secondary source (%s)\ndid not find(%s)(%s) meta data dictionary cannot be returned" % (source
% cfile
, cfile
['filename'], cfile
['inetref']))
3739 return available_metadata
3742 tmp_array
=tmp_files
.split('\n')
3743 for element
in tmp_array
:
3744 element
= (element
.rstrip('\n')).strip()
3745 if element
== '' or element
== None:
3748 index
= element
.index(':')
3751 key
= element
[:index
].lower()
3752 data
= element
[index
+1:]
3753 if data
== None or data
== '':
3755 if key
== u
'inetref' and len(cfile
['inetref']) == 7:
3756 meta_dict
[key
] = cfile
['inetref']
3758 data
= self
._changeAmp
(data
)
3759 data
= self
._changeToCommas
(data
)
3762 meta_dict
[key
] = int(data
)
3766 if key
== 'userrating':
3768 meta_dict
[key
] = float(data
)
3772 if key
== 'runtime':
3774 meta_dict
['length'] = long(data
)
3778 if key
== 'movierating':
3779 meta_dict
['rating'] = data
3783 if len(data
.split(' ')) < 10: # Skip plots that are less than 10 words
3787 if key
== 'trailer':
3789 if key
== 'releasedate':
3791 meta_dict
[key
] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
3795 meta_dict
[key
] = data
3796 if not len(meta_dict
):
3797 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']))
3798 return available_metadata
3801 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=movie
)
3802 self
.num_secondary_source_metadata_downloaded
+=1
3803 return available_metadata
3804 # end _getSecondarySourceMetadata
3806 def _getTmdbMetadata(self
, cfile
, available_metadata
):
3807 '''Download a movie's meta data and massage the genres string
3808 return results for secondary sources when no primary source meta data
3809 return dictionary of metadata combined with data from a secondary source
3812 if len(cfile
['inetref']) == 7: # IMDB number
3813 meta_dict
= self
.config
['tmdb_api'].searchIMDB(cfile
['inetref'])
3815 meta_dict
= self
.config
['tmdb_api'].searchTMDB(cfile
['inetref'])
3816 except TmdbMovieOrPersonNotFound
, e
:
3817 self
._displayMessage
(u
"0-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3818 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3819 except Exception, e
:
3820 self
._displayMessage
(u
"themoviedb.com error for Movie(%s)(%s) meta data dictionary cannot be returned, error(%s)" % (cfile
['filename'], cfile
['inetref'], e
))
3821 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3823 if meta_dict
== None:
3824 self
._displayMessage
(u
"1-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3825 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3827 keys
= meta_dict
.keys()
3830 data
= meta_dict
[key
]
3833 if key
== 'homepage':
3835 data
= self
._changeAmp
(data
)
3836 data
= self
._changeToCommas
(data
)
3839 genre_array
= data
.split(',')
3840 for i
in range(len(genre_array
)):
3841 genre_array
[i
] = (genre_array
[i
].strip()).lower()
3842 if genre_array
[i
] in self
.config
['tmdb_genre_filter']:
3843 genres
+=genre_array
[i
].title()+','
3845 meta_dict
[key
] = u
''
3848 meta_dict
[key
] = genres
[:-1]
3849 if key
== 'trailer':
3853 meta_dict
[key
] = int(data
)
3857 if key
== 'userrating':
3859 meta_dict
[key
] = float(data
)
3864 meta_dict
['homepage'] = data
3866 if key
== 'releasedate':
3868 meta_dict
[key
] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
3872 if key
== 'runtime':
3874 meta_dict
['length'] = long(data
)
3878 if meta_dict
.has_key('rating'):
3879 if meta_dict
['rating'] == '':
3880 meta_dict
['rating'] = 'Unknown'
3883 if available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None:
3884 filename
= u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
3885 meta_dict
['hash'] = self
.hashFile(filename
)
3886 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=True)
3887 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3889 self
._displayMessage
(u
"2-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile
['filename'], cfile
['inetref']))
3890 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
3891 # end _getTmdbMetadata
3893 def _getTvdbGraphics(self
, cfile
, graphic_type
, toprated
=False, watched
=False):
3894 '''Download either a TV Series Poster, Banner, Fanart or Episode image
3896 return full qualified path and filename of downloaded graphic
3898 rel_type
= graphic_type
3899 if graphic_type
== u
'coverfile':
3900 graphic_type
= u
'poster'
3901 elif graphic_type
== u
'poster':
3902 rel_type
=u
'coverfile'
3904 self
.config
['g_defaultname']=False
3905 self
.config
['toprated'] = toprated
3906 self
.config
['nokeys'] = False
3907 self
.config
['maximum'] = u
'1'
3910 self
.config
['sid']=cfile
['inetref']
3912 self
.config
['sid']=None
3913 self
.config
['episode_name'] = None
3914 self
.config
['series_name']=cfile
['file_seriesname']
3916 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3917 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3919 # Special logic must be used if the (-MG) guessing option has been requested
3920 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
3922 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
3923 except Exception, e
:
3924 self
._displayMessage
(u
"tvdb Series not found(%s) or connection issues with thetvdb.com web site.\nError:(%s)\n" % (cfile
['filename'], e
))
3926 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
,cfile
['file_seriesname'].lower()):
3927 self
.config
['sid'] = allmatchingseries
['sid']
3928 self
.config
['series_name'] = allmatchingseries
['name']
3929 cfile
['file_seriesname'] = allmatchingseries
['name']
3931 sys
.stderr
.write(u
"\nGuessing could not find an exact match on tvdb for Series (%s)\ntherefore a graphics cannot be downloaded\n\n" % cfile
['filename'])
3934 if not self
.verifySeriesExists():
3935 self
._displayMessage
(u
"tvdb Series not found(%s)" % cfile
['filename'])
3939 if self
.program_seriesid
== None:
3940 self
.config
['g_series'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season 1'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3941 self
.config
['g_season'] = self
.sanitiseFileName(cfile
['file_seriesname'])+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3943 self
.config
['g_series'] = self
.sanitiseFileName(self
.program_seriesid
)+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3944 self
.config
['g_season'] = self
.sanitiseFileName(self
.program_seriesid
)+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3946 # TV Series ALWAYS need the ' Season' in the file name incase the show name could clobber a Movie image
3947 # Season X is used so that a real season image is not overritten. It will be renamed later.
3948 self
.config
['g_series'] = self
.sanitiseFileName(self
.config
['series_name'])+u
' Season X'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3949 self
.config
['g_season'] = self
.sanitiseFileName(self
.config
['series_name'])+u
' Season %(seasonnumber)d'+self
.graphic_suffix
[rel_type
]+u
'.%(ext)s'
3951 typegetGraphics
=self
.getTopRatedGraphics
3952 self
.config
['season_num']= None # Needed to get toprated graphics named in 'g_series' format
3954 typegetGraphics
=self
.getGraphics
3956 self
.config
['overwrite'] = True # Force overwriting any existing graphic file
3957 value
= self
._downloadGraphics
(typegetGraphics(graphic_type
), mythtv
=True)
3958 self
.config
['overwrite'] = False # Turn off overwriting
3962 return self
.rtnRelativePath(value
, graphicsDirectories
[rel_type
])
3963 # end _getTvdbGraphics
3965 def _getTvdbMetadata(self
, cfile
, available_metadata
):
3966 '''Download thetvdb.com meta data
3967 return what was input or results from a secondary source
3968 return dictionary of metadata
3970 global video_type
, UI_title
3971 video_type
=u
'TV series'
3972 UI_title
= cfile
['file_seriesname']
3975 self
.config
['nokeys'] = False
3976 self
.config
['sid']=None
3977 self
.config
['episode_name'] = None
3978 self
.config
['series_name']=cfile
['file_seriesname']
3979 self
.config
['season_num']=u
"%d" % cfile
['seasno']
3980 self
.config
['episode_num']=u
"%d" % cfile
['epno']
3981 if self
.config
['series_name_override']:
3982 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
3983 self
.config
['sid'] = (self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]).strip()
3985 # Special logic must be used if the (-MG) guessing option has been requested
3986 if not self
.config
['sid'] and self
.config
['mythtv_guess']:
3988 allmatchingseries
= self
.config
['tvdb_api']._getSeries
(self
.config
['series_name'])
3989 except Exception, e
:
3990 self
._displayMessage
(u
"tvdb Series not found(%s) or there are connection problems with thetvdb.com\nError(%s)" % (cfile
['filename'], e
))
3992 if filter(is_not_punct_char
, allmatchingseries
['name'].lower()) == filter(is_not_punct_char
,cfile
['file_seriesname'].lower()):
3993 self
.config
['sid'] = allmatchingseries
['sid']
3994 self
.config
['series_name'] = allmatchingseries
['name']
3995 cfile
['file_seriesname'] = allmatchingseries
['name']
3997 sys
.stderr
.write(u
"\nGuessing could not find an exact match on tvdb for Series (%s)\ntherefore a meta data dictionary cannot be returned\n\n" % cfile
['filename'])
4000 if not self
.verifySeriesExists():
4001 self
._displayMessage
(u
"tvdb Series not found(%s) meta data dictionary cannot be returned" % cfile
['filename'])
4002 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4004 if self
.config
['sid'] == '99999999':
4005 if not self
.config
['interactive']:
4006 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4008 return {'sid': self.config['sid'], 'title': cfile['file_seriesname']}
4011 tmp_array
=(self
.getSeriesEpisodeData()).split('\n')
4013 for element
in tmp_array
:
4014 element
= (element
.rstrip('\n')).strip()
4017 index
= element
.index(':')
4018 key
= element
[:index
].lower()
4019 data
= element
[index
+1:]
4023 meta_dict
['title'] = data
4025 if key
== 'seasonnumber':
4027 meta_dict
['season'] = int(data
)
4031 if key
== 'episodenumber':
4033 meta_dict
['episode'] = int(data
)
4037 if key
== 'episodename':
4038 meta_dict
['subtitle'] = data
4040 if key
== u
'overview':
4041 meta_dict
['plot'] = data
4043 if key
== u
'director' and data
== 'None':
4044 meta_dict
['director'] = ''
4046 if key
== u
'firstaired' and len(data
) > 4:
4048 meta_dict
['year'] = int(data
[:4])
4051 meta_dict
['firstaired'] = data
4053 meta_dict
['releasedate'] = datetime
.datetime
.strptime(data
,'%Y-%m-%d').date()
4059 meta_dict
['year'] = int(data
)
4063 if key
== 'seriesid':
4064 meta_dict
['inetref'] = data
4065 meta_dict
[key
] = data
4069 meta_dict
['userrating'] = float(data
)
4073 if key
== 'filename':# This "episodeimage URL clashed with the video file name and ep image
4074 continue # is not used yet. So skip fixes the db video filename from being wiped.
4075 if key
== 'runtime':
4077 meta_dict
['length'] = long(data
)
4081 meta_dict
[key
] = data
4084 if not meta_dict
.has_key('director'):
4085 meta_dict
['director'] = u
''
4086 meta_dict
['rating'] = u
'TV Show'
4087 # URL to TVDB web site episode web page for this series
4088 for url_data
in [u
'seriesid', u
'seasonid', u
'id']:
4089 if not url_data
in meta_dict
.keys():
4092 meta_dict
['homepage'] = u
'http://www.thetvdb.com/?tab=episode&seriesid=%s&seasonid=%s&id=%s' % (meta_dict
['seriesid'], meta_dict
['seasonid'], meta_dict
['id'])
4093 if available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None:
4094 filename
= u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4095 meta_dict
['hash'] = self
.hashFile(filename
)
4096 available_metadata
= self
.combineMetaData(available_metadata
, meta_dict
, vid_type
=False)
4097 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4099 self
._displayMessage
(u
"tvdb Series found (%s) but no meta data for dictionary" % cfile
['filename'])
4100 return self
._getSecondarySourceMetadata
(cfile
, available_metadata
)
4101 # end _getTvdbMetadata
4103 def _make_db_ready(self
, text
):
4104 '''Prepare text for inclusion into a DB
4106 return data base ready text
4108 if not text
: return text
4110 text
= text
.replace(u
'\u2013', u
"-")
4111 text
= text
.replace(u
'\u2014', u
"-")
4112 text
= text
.replace(u
'\u2018', u
"'")
4113 text
= text
.replace(u
'\u2019', u
"'")
4114 text
= text
.replace(u
'\u2026', u
"...")
4115 text
= text
.replace(u
'\u201c', u
'"')
4116 text
= text
.replace(u
'\u201d', u
'"')
4117 except UnicodeDecodeError:
4123 def _addCastGenreCountry(self
, data_string
, vim
, cast_genres_type
):
4124 '''From a comma delimited string of cast members, genres or countries add the ones
4125 not already in the myth db and update the video's meta data
4126 return True when successfull
4127 return False if failed
4129 if data_string
== '':
4131 data
= data_string
.split(',')
4132 for i
in range(len(data
)):
4133 data
[i
]=data
[i
].strip()
4139 if cast_genres_type
== 'genres':
4142 elif cast_genres_type
== 'cast':
4145 elif cast_genres_type
== 'countries':
4147 vim
.country
.add(item
)
4150 # end _addCastGenreCountry()
4156 def _moveDirectoryTree(self
, src
, dst
, symlinks
=False, ignore
=None):
4157 '''Move a directory tree from a given source to a given destination. Subdirectories will be
4158 created and synbolic links will be recreated in the new destination.
4159 return an array of two arrays. Names of files/directories moved and Errors found
4165 (src
, fileName
) = os
.path
.split(src
)
4167 names
= os
.listdir(unicode(src
, 'utf8'))
4168 except (UnicodeEncodeError, TypeError):
4169 names
= os
.listdir(src
)
4171 if os
.path
.isfile(src
):
4172 (src
, fileName
) = os
.path
.split(src
)
4176 names
= os
.listdir(unicode(src
, 'utf8'))
4177 except (UnicodeEncodeError, TypeError):
4178 names
= os
.listdir(src
)
4180 if ignore
is not None:
4181 ignored_names
= ignore(src
, names
)
4183 ignored_names
= set()
4186 if self
.config
['simulation']:
4187 sys
.stdout
.write(u
"Simulation creating subdirectories for file move (%s)\n" % dst
)
4189 self
._displayMessage
(u
"Creating subdirectories for file move (%s)\n" % dst
)
4190 os
.makedirs(dst
) # Some of the subdirectories may already exist
4195 if name
in ignored_names
:
4197 srcname
= os
.path
.join(src
, name
)
4198 dstname
= os
.path
.join(dst
, name
)
4200 if not os
.access(srcname
, os
.F_OK | os
.R_OK | os
.W_OK
): # Skip any file that is not RW able
4201 sys
.stderr
.write(u
"\n! Error: The Source video directory or file (%s) must have read and write permissions for be moved. File or directory has been skipped\n" % (srcname
))
4204 if symlinks
and os
.path
.islink(srcname
):
4205 linkto
= os
.readlink(srcname
)
4206 if self
.config
['simulation']:
4207 sys
.stdout
.write(u
"Simulation recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto
, dstname
))
4209 os
.symlink(linkto
, dstname
)
4210 self
._displayMessage
(u
"Recreating symbolic link linkto:\n(%s) destination name:\n(%s)\n" % (linkto
, dstname
))
4211 self
.num_symbolic_links
+=1
4212 elif os
.path
.isdir(srcname
):
4214 self
._displayMessage
(u
"Wildcard skipping subdirectory (%s)\n" % srcname
)
4216 self
.num_created_video_subdirectories
+=1
4217 self
._displayMessage
(u
"Move subdirectory (%s)\n" % srcname
)
4218 self
._moveDirectoryTree
(srcname
, dstname
, symlinks
, ignore
)
4220 if self
.config
['simulation']:
4222 if srcname
.startswith(org_src
[:-1]):
4223 sys
.stdout
.write(u
"Simulation move wild card file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4224 self
.num_moved_video_files
+=1
4225 self
.new_names
.append(dstname
)
4227 self
._displayMessage
(u
"Simulation of wildcard skipping file(%s)" % (srcname
,))
4229 sys
.stdout
.write(u
"Simulation move file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4230 self
.num_moved_video_files
+=1
4231 self
.new_names
.append(dstname
)
4234 if srcname
.startswith(org_src
[:-1]):
4235 self
._displayMessage
(u
"Move wild card file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4236 shutil
.move(srcname
, dstname
)
4237 self
.num_moved_video_files
+=1
4238 self
.new_names
.append(dstname
)
4240 self
._displayMessage
(u
"Wildcard skipping file(%s)" % (srcname
,))
4242 self
._displayMessage
(u
"Move file from\n(%s) to\n(%s)\n" % (srcname
, dstname
))
4243 shutil
.move(srcname
, dstname
)
4244 self
.num_moved_video_files
+=1
4245 self
.new_names
.append(dstname
)
4246 # XXX What about devices, sockets etc.?
4247 except (IOError, os
.error
), why
:
4248 self
.errors
.append([srcname
, dstname
, str(why
)])
4249 # catch the Error from the recursive move tree so that we can
4250 # continue with other files
4252 self
.errors
.append([src
, dst
, u
"Unknown error"])
4254 return [self
.new_names
, self
.errors
]
4255 # end _moveDirectoryTree
4257 # local variable for move stats
4258 num_moved_video_files
=0
4259 num_created_video_subdirectories
=0
4260 num_symbolic_links
=0
4262 def _moveVideoFiles(self
, target_destination_array
):
4263 """Copy files or directories to a destination directory.
4264 If the -F filename option is set then rename TV series during the move process. The move will
4265 be interactive for identifying a movie's IMDB number or TV series if the -i option was also set.
4266 If there is a problem error message are displayed and the script exists. After processing
4267 print a statistics report.
4268 return a array of video file dictionaries to update in Mythvideo data base
4270 global UI_selectedtitle
4271 # Validate that the targets and destinations actually exist.
4273 for file_dir
in target_destination_array
:
4274 if os
.access(file_dir
, os
.F_OK | os
.R_OK
):
4276 # Destinations must all be directories
4277 if not os
.path
.isdir(file_dir
):
4278 sys
.stderr
.write(u
"\n! Error: Destinations must all be directories.\nThis destination is not a directory (%s)\n" % (file_dir
,))
4282 for directory
in self
.config
['mythvideo']:
4283 dummy_dir
= file_dir
.replace(directory
, u
'')
4284 if dummy_dir
!= tmp_dir
:
4287 sys
.stderr
.write(u
"\n! Error: Destinations must all be a mythvideo directory or subdirectory.\nThis destination (%s) is not one of the Mythvideo directories(%s)\n" % (file_dir
, self
.config
['mythvideo'], ))
4289 # Verify that a target file is really a video file.
4290 if file_dir
[-1:] != '*': # Skip wildcard file name targets
4291 if os
.access(file_dir
, os
.F_OK | os
.R_OK
): # Confirm that the file actually exists
4292 if not os
.path
.isdir(file_dir
):
4293 ext
= _getExtention(file_dir
)
4294 for tmp_ext
in self
.config
['video_file_exts']:
4295 if ext
.lower() == tmp_ext
:
4298 sys
.stderr
.write(u
"\n! Error: Target files must be video files(%s).\nSupported video file extentions(%s)\n" % (file_dir
, self
.config
['video_file_exts'],))
4303 num_renamed_files
= 0
4304 num_mythdb_updates
= 0
4307 video_files_to_process
=[]
4309 while i
< len(target_destination_array
):
4310 src
= target_destination_array
[i
]
4312 if src
[-1:] == u
'*':
4315 (src
, fileName
) = os
.path
.split(src
)
4316 dst
= target_destination_array
[i
+1]
4320 results
= self
._moveDirectoryTree
(org_src
, dst
, symlinks
=False, ignore
=None)
4322 results
= self
._moveDirectoryTree
(src
, dst
, symlinks
=False, ignore
=None)
4323 if len(results
[1]): # Check if there are any errors
4324 sys
.stderr
.write(u
"\n! Warning: There were errors during moving, with these directories/files\n")
4325 for error
in results
[1]:
4326 sys
.stderr
.write(u
'\n! Warning: Source(%s), Destination(%s), Reason:(%s)\n' % (error
[0], error
[1], error
[2]))
4328 for name
in results
[0]:
4329 file_name
= os
.path
.join(dst
, name
)
4330 if os
.path
.isdir(file_name
):
4331 for dictionary
in self
._processNames
(_getFileList([file_name
]), verbose
= self
.config
['debug_enabled'], movies
=True):
4332 tmp_cfile_array
.append(dictionary
)
4334 for dictionary
in self
._processNames
([file_name
], verbose
= self
.config
['debug_enabled'], movies
=True):
4335 tmp_cfile_array
.append(dictionary
)
4337 # Is the source directory within a mythvideo directory? If it is,
4338 # update existing mythdb records else add the record as you already have the inetref
4339 for directory
in self
.config
['mythvideo']:
4340 if src
.startswith(directory
):
4341 for cfile
in tmp_cfile_array
:
4342 tmp_path
= src
+cfile
['filepath'].replace(dst
, u
'')
4343 video_file
= self
.rtnRelativePath(self
.movie_file_format
% (tmp_path
, cfile
['filename'], cfile
['ext']), 'mythvideo')
4344 tmp_filename
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), 'mythvideo')
4345 result
= mythvideo
.getVideo(exactfile
=video_file
)
4349 intid
= result
.intid
4351 result
= mythvideo
.getVideo(exactfile
=self
.movie_file_format
% (tmp_path
, cfile
['filename'], cfile
['ext']), host
=localhostname
.lower())
4355 intid
= result
.intid
4357 metadata
= Video(id=intid
, db
=mythvideo
)
4358 if tmp_filename
[0] == '/':
4360 self
.absolutepath
= True
4362 host
= localhostname
.lower()
4363 self
.absolutepath
= False
4365 if self
.config
['simulation']:
4366 sys
.stdout
.write(u
"Simulation Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file
, tmp_filename
))
4368 self
._displayMessage
(u
"Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file
, tmp_filename
))
4369 Video(id=intid
, db
=mythvideo
).update({'filename': tmp_filename, 'host': host}
)
4370 num_mythdb_updates
+=1
4374 cfile_array
.extend(tmp_cfile_array
)
4375 i
+=2 # Increment by 2 because array is int pairs of target and destination
4377 # Attempt to rename the video file
4378 if self
.config
['ret_filename']:
4379 for index
in range(len(cfile_array
)):
4380 cfile
= cfile_array
[index
]
4381 if self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']:
4382 sys
.stdout
.write(u
"\nAttempting to rename video filename (%s)\n" % cfile
['file_seriesname'])
4383 if cfile
['seasno'] == 0 and cfile
['epno'] == 0: # File rename for a movie
4386 if self
.config
['series_name_override']:
4387 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
4388 sid
= self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]
4390 data
= self
._getTmdbIMDB
(cfile
['file_seriesname'], rtnyear
=True)
4393 if data
[u
'sid'] == '99999999': # The user chose to ignore this video
4395 new_filename
= self
.sanitiseFileName(data
[u
'name'])
4399 imdb_access
= imdb
.IMDb()
4401 data
= imdb_access
.get_movie(sid
)
4402 if data
.has_key('long imdb title'):
4403 new_filename
= data
['long imdb title']
4404 elif data
.has_key('title'):
4405 new_filename
= self
.sanitiseFileName(namedata
['title'])
4408 except imdb
._exceptions
.IMDbDataAccessError
:
4411 if not sid
: # Cannot find this movie skip the renaming
4414 if not new_filename
:
4417 cfile_array
[index
]['file_seriesname'] = new_filename
4418 else: # File rename for a TV Series Episode
4419 UI_selectedtitle
= u
''
4421 self
.config
['sid'] = None
4422 self
.config
['series_name'] = cfile
['file_seriesname']
4423 if self
.config
['series_name_override']:
4424 if self
.config
['series_name_override'].has_key(cfile
['file_seriesname'].lower()):
4425 self
.config
['sid'] = self
.config
['series_name_override'][cfile
['file_seriesname'].lower()]
4426 self
.config
['series_name'] = None
4427 self
.config
['season_num'] = u
"%d" % cfile
['seasno']
4428 self
.config
['episode_num'] = u
"%d" % cfile
['epno']
4429 self
.config
['episode_name'] = None
4430 new_filename
= self
.returnFilename()
4431 inetref
= self
.config
['sid']
4432 if inetref
== '99999999': # User chose to ignore this video
4436 if new_filename
== cfile
['filename']: # The file was already named to standard format
4437 self
._displayMessage
(u
"File is already the correct name(%s)\n" % cfile
['filename'])
4439 video_file
= self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4440 tmp_filename
= self
.movie_file_format
% (cfile
['filepath'], new_filename
, cfile
['ext'])
4441 if self
.config
['simulation']:
4442 sys
.stdout
.write(u
"Simulation file renamed from(%s) to(%s)\n" % (video_file
, tmp_filename
))
4444 if not os
.access(video_file
, os
.F_OK | os
.R_OK | os
.W_OK
):
4445 sys
.stdout
.write(u
"Cannot rename this file as it does not have read/write permissions set (%s)\n" % video_file
)
4447 self
._displayMessage
(u
"File renamed from(%s) to(%s)\n" % (video_file
, tmp_filename
))
4448 os
.rename(video_file
, tmp_filename
)
4449 num_renamed_files
+=1
4450 video_file
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), 'mythvideo')
4451 tmp_filename
= self
.rtnRelativePath(self
.movie_file_format
% (cfile
['filepath'], new_filename
, cfile
['ext']), 'mythvideo')
4452 result
= mythvideo
.getVideo(exactfile
=video_file
)
4456 intid
= result
.intid
4458 result
= mythvideo
.getVideo(exactfile
=self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext']), host
=localhostname
.lower())
4462 intid
= result
.intid
4463 if tmp_filename
[0] == '/':
4465 self
.absolutepath
= True
4467 host
= localhostname
.lower()
4468 self
.absolutepath
= False
4470 metadata
= Video(id=intid
, db
=mythvideo
)
4471 if self
.config
['simulation']:
4472 sys
.stdout
.write(u
"Simulation Mythdb update for renamed file(%s)\n" % (tmp_filename
))
4474 self
._displayMessage
(u
"Mythdb update for renamed file(%s)\n" % (tmp_filename
))
4475 Video(id=intid
, db
=mythvideo
).update({'filename': tmp_filename, 'host': host}
)
4477 if self
.config
['simulation']:
4478 sys
.stdout
.write(u
"Simulation Mythdb add for renamed file(%s)\n" % (tmp_filename
))
4480 self
._displayMessage
(u
"Adding Mythdb record for file(%s)\n" % (tmp_filename
))
4482 initrec
[u
'title'] = cfile
['file_seriesname']
4483 initrec
[u
'filename'] = tmp_filename
4484 initrec
[u
'host'] = host
4485 initrec
[u
'inetref'] = inetref
4486 Video(db
=mythvideo
).create(initrec
)
4487 cfile_array
[index
]['filename'] = new_filename
4489 if self
.config
['simulation']:
4490 sys
.stdout
.write(u
'\n---------Simulated Statistics---------------')
4491 sys
.stdout
.write('\n--------------Move Statistics---------------\nNumber of subdirectories ............(% 5d)\nNumber of files moved ...............(% 5d)\nNumber of symbolic links recreated...(% 5d)\nNumber of renamed TV-eps or movies.. (% 5d)\nNumber of Myth database updates .... (% 5d)\n--------------------------------------------\n\n' % (self
.num_created_video_subdirectories
, self
.num_moved_video_files
, self
.num_symbolic_links
, num_renamed_files
, num_mythdb_updates
))
4494 # end _moveVideoFiles
4496 def _displayMessage(self
, message
):
4497 """Displays messages through stdout. Usually used with MythTv metadata updates in -V
4501 if message
[-1:] != '\n':
4503 if self
.config
['mythtv_verbose']:
4504 sys
.stdout
.write(message
)
4505 # end _displayMessage
4507 def _findMissingInetref(self
):
4508 '''Find any video file without a Mythdb record or without an inetref number
4509 return None if there are no new video files
4510 return a array of dictionary information on each video file that qualifies for processing
4512 directories
=self
.config
['mythvideo']
4514 if not len(directories
):
4515 sys
.stderr
.write(u
"\n! Error: There must be a video directory specified in MythTv\n")
4518 allFiles
= self
._findFiles
(directories
, self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
4519 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'], movies
=True)
4520 if len(validFiles
) == 0: # Is there video files to process?
4524 for cfile
in validFiles
:
4526 videopath
= self
.movie_file_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
4527 except UnicodeDecodeError:
4528 videopath
= os
.path
.join(unicode(cfile
['filepath'],'utf8'), unicode(cfile
['filename'],'utf8')+u
'.'+cfile
['ext'])
4530 # Find the MythTV meta data
4531 result
= mythvideo
.getVideo(exactfile
=videopath
)
4535 intid
= result
.intid
4537 result
= mythvideo
.getVideo(exactfile
=self
.rtnRelativePath(videopath
, 'mythvideo'), host
=localhostname
.lower())
4541 intid
= result
.intid
4543 missing_list
.append(cfile
)
4545 meta_dict
= Video(id=intid
, db
=mythvideo
)
4546 if self
.config
['video_dir']:
4547 if not mythvideo
.getVideo(exactfile
=meta_dict
[u
'filename'], host
=meta_dict
[u
'host']):
4548 missing_list
.append(cfile
)
4550 # There must be an Internet reference number. Get one for new records.
4551 if _can_int(meta_dict
['inetref']) and not meta_dict
['inetref'] == u
'00000000' and not meta_dict
['inetref'] == '':
4553 missing_list
.append(cfile
)
4556 # end _findMissingInetref
4558 def _checkValidGraphicFile(self
, filename
, graphicstype
=u
'', vidintid
=False):
4559 '''Verify that a graphics file is not really an HTML file
4560 return True if it is a graphics file
4561 return False if it is an HTML file
4563 # Verify that the graphics file is NOT HTML instead of the intended graphics file
4565 p
= subprocess
.Popen(u
'file "%s"' % filename
, shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
4567 # There is something wrong with the file but do NOT say it is invalid just in case!
4569 data
= p
.stdout
.readline()
4571 data
= data
.encode('utf8')
4572 except UnicodeDecodeError:
4573 data
= unicode(data
,'utf8')
4574 index
= data
.find(u
'HTML document text')
4577 elif self
.config
['simulation']:
4579 u
"Simulation deleting bad graphics file (%s) as it is really HTML\n" % (filename
, )
4583 u
"and the MythVideo record was corrected for the graphic reference.\n"
4587 os
.remove(filename
) # Delete the useless HTML text
4588 sys
.stderr
.write( u
"\n! Warning: The graphics file (%s) is actually HTML and not the intended file type.\nDuring the original file download the web site had issues. The bad downloaded file was removed.\n" % (filename
))
4591 if graphicstype
== u
'coverfile':
4592 repair
[graphicstype
] = u
'No Cover'
4594 repair
[graphicstype
] = u
''
4595 Video(id=vidintid
, db
=mythvideo
).update(repair
)
4597 # end _checkValidGraphicFile()
4600 def _graphicsCleanup(self
):
4601 '''Match the graphics in the mythtv graphics directories with the ones specified by the
4602 mythvideometa records. Remove any graphics that are not referenced at least once. Print a
4605 global localhostname
4609 stats
= {'coverfile': [0,0,0], 'banner': [0,0,0], 'fanart': [0,0,0]}
4611 graphics_file_dict
={}
4612 all_graphics_file_list
=[]
4613 for directory
in graphicsDirectories
.keys():
4614 if directory
== 'screenshot':
4616 file_list
= _getFileList(self
.config
[graphicsDirectories
[directory
]])
4617 if not len(file_list
):
4618 graphics_file_dict
[directory
] = []
4620 for g_file
in list(file_list
): # Cull the list removing dirs and non-graphics files
4621 if os
.path
.isdir(g_file
):
4622 file_list
.remove(g_file
)
4624 g_ext
= _getExtention(g_file
)
4625 if not g_ext
in self
.image_extensions
:
4626 file_list
.remove(g_file
)
4628 for filel
in file_list
:
4629 if not filel
in all_graphics_file_list
:
4630 all_graphics_file_list
.append(filel
)
4631 graphics_file_dict
[directory
] = file_list
4633 for key
in graphicsDirectories
.keys(): # Set initial totals
4634 if key
== 'screenshot':
4636 stats
[key
][num_total
] = len(graphics_file_dict
[key
])
4638 # Start reading videometadata records to remove their graphics from the image orphan list
4640 records
= mythvideo
.searchVideos()
4641 except MythError
, e
:
4642 sys
.stderr
.write(u
"\n! Error: Reading all videometadata records: %s\n" % e
.args
[0])
4645 atleast_one_video_file
= False
4647 for record
in records
:
4648 atleast_one_video_file
= True
4649 meta_dict
= {'host': record.host, 'coverfile': record.coverfile, 'banner': record.banner, 'fanart': record.fanart, 'filename': record.filename, 'intid': record.intid, 'inetref': record.inetref, }
4650 # Skip any videometadata record that is not for this host
4651 if meta_dict
['host'] != u
'' and meta_dict
['host'] != None:
4652 if meta_dict
['host'].lower() != localhostname
.lower():
4654 # Start removing any graphics in this videometadata record
4655 for key
in meta_dict
.keys():
4656 if key
in ['host','filename','intid', 'inetref']:
4658 if meta_dict
[key
] in [None, u
'', u
'None', u
'No Cover', u
'Unknown']:
4661 # Deal with videometadata record using storage groups
4662 if meta_dict
['filename'] != None:
4663 if meta_dict
['filename'][0] == u
'/':
4664 self
.absolutepath
= True
4666 self
.absolutepath
= False
4667 if meta_dict
[key
][0] != '/':
4668 meta_dict
[key
] = self
.rtnAbsolutePath(meta_dict
[key
], graphicsDirectories
[key
])
4669 if meta_dict
[key
][0] != '/': # There is not a storage group for this relative file name
4672 # Deal with TV series level graphics
4673 (dirName
, fileName
) = os
.path
.split(meta_dict
[key
])
4674 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
4675 index
= fileBaseName
.find(u
' Season ')
4676 intid
= meta_dict
['intid']
4678 if index
!= -1: # Is this a TV Series episode?
4679 if meta_dict
[key
] in graphics_file_dict
[key
]:
4680 if self
._checkValidGraphicFile
(meta_dict
[key
], graphicstype
=key
, vidintid
=intid
) == True:
4681 graphics_file_dict
[key
].remove(meta_dict
[key
])
4682 all_graphics_file_list
.remove(meta_dict
[key
])
4683 # This logic is specific to Movies and videos with a '99999999' inetref numbers
4684 elif fileName
.startswith(meta_dict
['inetref']+u
'_') or fileName
.startswith(meta_dict
['inetref']+u
'.') or meta_dict
['inetref'] == '99999999':
4685 if meta_dict
[key
] in graphics_file_dict
[key
]:
4686 if self
._checkValidGraphicFile
(meta_dict
[key
], graphicstype
=key
, vidintid
=intid
) == True:
4687 graphics_file_dict
[key
].remove(meta_dict
[key
])
4688 all_graphics_file_list
.remove(meta_dict
[key
])
4690 if not atleast_one_video_file
:
4691 sys
.stderr
.write(u
"\n! Error: Janitor - did not find any video files to process so skipping\nimage clean up to protect your image files, in case this is a configuration or NFS error.\nIf you do not use MythVideo then the Janitor option (-MJ) is not of value to you on this MythTV back end.\n")
4693 # end reading videometadata records to remove their graphics from the image orphan list
4695 # Get Scheduled and Recorded program list
4696 programs
= self
._getScheduledRecordedProgramList
()
4698 # Remove Scheduled and Recorded program's graphics files from the delete list
4700 for field
in graphicsDirectories
.keys():
4701 if field
== 'screenshot':
4704 for graphic
in graphics_file_dict
[field
]:
4705 (dirName
, fileName
) = os
.path
.split(graphic
)
4706 (fileBaseName
, fileExtension
)=os
.path
.splitext(fileName
)
4707 for program
in programs
:
4708 if fileBaseName
.lower().startswith(program
['title'].lower()+u
' '):
4709 remove
.append(graphic
)
4711 if not isValidPosixFilename(program
['title']) and program
['seriesid'] != u
'':
4712 if fileBaseName
.lower().startswith(program
['seriesid'].lower()):
4713 remove
.append(graphic
)
4716 if self
._checkValidGraphicFile
(rem
, graphicstype
=u
'', vidintid
=False) == True:
4717 graphics_file_dict
[field
].remove(rem
)
4719 all_graphics_file_list
.remove(rem
)
4720 except ValueError, e
:
4723 # Do not remove the MiroBridge default image files even if they are not currently being used
4724 for filel
in list(all_graphics_file_list
):
4725 if filel
.endswith('mirobridge_coverart.jpg'):
4726 all_graphics_file_list
.remove(filel
)
4728 if filel
.endswith('mirobridge_banner.jpg'):
4729 all_graphics_file_list
.remove(filel
)
4731 if filel
.endswith('mirobridge_fanart.jpg'):
4732 all_graphics_file_list
.remove(filel
)
4735 for key
in graphicsDirectories
.keys(): # Set deleted files totals
4736 if key
== 'screenshot':
4738 file_list
= list(graphics_file_dict
[key
])
4739 for filel
in file_list
:
4740 if not filel
in all_graphics_file_list
:
4741 graphics_file_dict
[key
].remove(filel
)
4742 stats
[key
][num_deleted
] = len(graphics_file_dict
[key
])
4744 # Delete all graphics files still on the delete list
4745 for filel
in all_graphics_file_list
:
4746 if self
.config
['simulation']:
4748 u
"Simulation deleting (%s)\n" % (filel
)
4755 self
._displayMessage
(u
"(%s) Has been deleted\n" % (filel
))
4757 for key
in graphicsDirectories
.keys(): # Set new files totals
4758 if key
== 'screenshot':
4760 stats
[key
][num_new_total
] = stats
[key
][num_total
] - stats
[key
][num_deleted
]
4762 if self
.config
['simulation']:
4763 sys
.stdout
.write(u
'\n\n------------Simulated Statistics---------------')
4764 sys
.stdout
.write(u
'\n--------------Janitor Statistics---------------\n')
4765 stat_type
= ['total', 'deleted', 'new total']
4766 for index
in range(len(stat_type
)):
4767 for key
in graphicsDirectories
.keys(): # Print stats
4768 if key
== 'screenshot':
4770 if key
== 'coverfile':
4774 sys
.stdout
.write(u
'% 9s % 7s ......................(% 5d)\n' % (stat_type
[index
], g_type
, stats
[key
][index
], ))
4776 for key
in graphicsDirectories
.keys(): # Print stats
4777 if key
== 'screenshot':
4779 if not len(graphics_file_dict
[key
]):
4781 if key
== 'coverfile':
4785 sys
.stdout
.write(u
'\n----------------Deleted %s files---------------\n' % g_type
)
4786 for graphic
in graphics_file_dict
[key
]:
4787 sys
.stdout
.write('%s\n' % graphic
)
4789 # end _graphicsCleanup
4791 def _getVideoLength(self
, videofilename
):
4792 '''Using ffmpeg (if it can be found) get the duration of the video
4793 return False if either ffmpeg cannot be found or the file is not a video
4794 return video lenght in minutes
4796 if not self
.config
['ffmpeg']:
4799 # Filter out specific file types due to potential negative processing overhead
4800 if _getExtention(videofilename
) in [u
'iso', u
'img', u
'VIDEO_TS', u
'm2ts', u
'vob']:
4803 p
= subprocess
.Popen(u
'ffmpeg -i "%s"' % (videofilename
), shell
=True, bufsize
=4096, stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
, close_fds
=True)
4807 data
= p
.stderr
.readline()
4808 if data
.endswith('not found\n'):
4809 ffmpeg_found
= False
4811 if data
.startswith(' Duration:'):
4813 if data
== '' and p
.poll() != None:
4816 if ffmpeg_found
== False:
4817 self
.config
['ffmpeg'] = False
4820 time
= (data
[data
.index(':')+1: data
.index('.')]).strip()
4821 return (60*(int(time
[:2]))+(int(time
[3:5])))
4824 # end _getVideoLength
4827 def _getMiroVideometadataRecords(self
):
4828 """Fetches all videometadata records with an inetref of '99999999' and a category of 'Miro'. If the
4829 videometadata record has a host them it must match the lower-case of the locahostname.
4830 aborts if processing failed
4831 return and array of matching videometadata dictionary records
4833 global localhostname
4836 records
= mythvideo
.searchVideos(category
=u
'Miro', custom
=(('inetref=%s','99999999'),))
4837 except MythError
, e
:
4838 sys
.stderr
.write(u
"\n! Error: Reading all Miro videometadata records: %s\n" % e
.args
[0])
4841 for record
in records
:
4842 intids
.append(record
.intid
)
4844 videometadatarecords
=[]
4846 for intid
in intids
:
4847 vidrec
= Video(id=intid
, db
=mythvideo
)
4848 if vidrec
[u
'host'] != u
'' and vidrec
[u
'host'] != None:
4849 if vidrec
[u
'host'].lower() != localhostname
.lower():
4851 videometadatarecords
.append(vidrec
)
4853 return videometadatarecords
4856 # end _getMiroVideometadataRecords()
4858 def _getExtraMiroDetails(self
, mythvideorec
, vidtype
):
4859 '''Find the extra details required for Miro MythVideo record processing
4860 return a dictionary of details required for processing
4863 extradata
[u
'intid'] = [mythvideorec
[u
'intid']]
4864 if vidtype
== u
'movies':
4865 extradata
[u
'tv'] = False
4867 extradata
[u
'tv'] = True
4869 for key
in [u
'coverfile', u
'banner', u
'fanart', ]:
4870 extradata
[key
] = True # Set each graphics type as if it has already been downloaded
4871 if mythvideorec
[key
] == None or mythvideorec
[key
] == u
'No Cover' or mythvideorec
[key
] == u
'':
4872 extradata
[key
] = False
4874 elif key
== u
'coverfile': # Look for undersized coverart
4875 if mythvideorec
[u
'filename'][0] == u
'/':
4876 self
.absolutepath
= True
4878 self
.absolutepath
= False
4879 filename
= self
.rtnAbsolutePath(mythvideorec
[key
], graphicsDirectories
[key
])
4881 (width
, height
) = self
.config
['image_library'].open(filename
).size
4882 if width
< self
.config
['min_poster_size']:
4883 extradata
[key
] = False
4886 extradata
[key
] = False
4889 else: # Check if the default graphics are being used
4890 if mythvideorec
[key
].endswith(u
'mirobridge_banner.jpg'):
4891 extradata
[key
] = False
4892 if mythvideorec
[key
].endswith(u
'mirobridge_fanart.jpg'):
4893 extradata
[key
] = False
4896 if vidtype
== u
'movies': # Data specific to Movie Trailers
4897 if mythvideorec
[u
'filename'][0] == u
'/':
4898 self
.absolutepath
= True
4900 self
.absolutepath
= False
4901 extradata
[u
'filename'] = mythvideorec
[u
'filename']
4902 extradata
[u
'pathfilename'] = self
.rtnAbsolutePath(mythvideorec
[u
'filename'], u
'mythvideo')
4903 if os
.path
.islink(extradata
[u
'pathfilename']):
4904 extradata
[u
'symlink'] = True
4906 extradata
[u
'symlink'] = False
4907 moviename
= mythvideorec
['subtitle']
4911 index
= moviename
.find(self
.config
[u
'mb_movies'][filter(is_not_punct_char
, mythvideorec
[u
'title'].lower())])
4913 moviename
= moviename
[:index
].strip()
4914 extradata
[u
'moviename'] = moviename
4915 extradata
[u
'inetref'] = False
4916 if not moviename
== None and not moviename
== '':
4917 lastyear
= int(datetime
.datetime
.now().strftime(u
"%Y"))
4920 while i
< 5: # Check for a Movie that will be released this year or the next four years
4921 years
.append(u
"%d" % ((lastyear
+i
)))
4923 imdb_access
= imdb
.IMDb()
4926 movies_found
= imdb_access
.search_movie(moviename
.encode("ascii", 'ignore'))
4930 for movie
in movies_found
: # Get rid of duplicates
4931 if movie
.has_key('year'):
4932 temp
= {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])}
4933 if tmp_movies
.has_key(temp
.keys()[0]):
4935 tmp_movies
[temp
.keys()[0]] = temp
[temp
.keys()[0]]
4937 for movie
in tmp_movies
:
4938 if filter(is_not_punct_char
, tmp_movies
[movie
][:-7].lower()) == filter(is_not_punct_char
, moviename
.lower()) and tmp_movies
[movie
][-5:-1] == year
:
4939 extradata
[u
'inetref'] = u
"%07d" % int(movie
)
4940 extradata
[u
'moviename'] = tmp_movies
[movie
]
4941 extradata
[u
'year'] = year
4943 if extradata
[u
'inetref']:
4946 # end _getExtraMiroDetails()
4948 def updateMiroVideo(self
, program
):
4949 '''Update the information in a Miro/MythVideo record
4952 global localhostname
, graphicsDirectories
4954 mirodetails
= program
[u
'miro']
4956 for intid
in mirodetails
[u
'intid']:
4958 for key
in graphicsDirectories
.keys():
4959 if key
== u
'screenshot':
4961 if mirodetails
[key
] != True and mirodetails
[key
] != False and mirodetails
[key
] != None and mirodetails
[key
] != u
'Simulated Secondary Source graphic filename place holder':
4962 # A graphics was downloaded
4963 changed_fields
[key
] = mirodetails
[key
]
4965 if not mirodetails
[u
'tv'] and not mirodetails
[u
'symlink'] and os
.access(mirodetails
[u
'pathfilename'], os
.F_OK | os
.R_OK | os
.W_OK
):
4966 changed_fields
[u
'inetref'] = mirodetails
[u
'inetref']
4967 changed_fields
[u
'subtitle'] = u
''
4968 changed_fields
[u
'year'] = mirodetails
[u
'year']
4969 changed_fields
[u
'banner'] = u
''
4970 (dirName
, fileName
) = os
.path
.split(mirodetails
[u
'pathfilename'])
4971 (fileBaseName
, fileExtension
) = os
.path
.splitext(fileName
)
4973 dir_list
= os
.listdir(unicode(dirName
, 'utf8'))
4974 except (UnicodeEncodeError, TypeError):
4975 dir_list
= os
.listdir(dirName
)
4978 filename
= self
.sanitiseFileName(u
'%s - Trailer %d' % (mirodetails
[u
'moviename'], index
))
4979 fullfilename
= u
'%s/%s%s' % (dirName
, filename
, fileExtension
)
4980 for flenme
in dir_list
:
4981 if fnmatch
.fnmatch(flenme
.lower(), u
'%s.*' % filename
.lower()):
4984 changed_fields
[u
'title'] = filename
4985 if self
.config
['simulation']:
4987 u
"Simulation rename Miro-MythTV movie trailer from (%s) to (%s)\n" % (mirodetails
[u
'pathfilename'], fullfilename
))
4989 os
.rename(mirodetails
[u
'pathfilename'], fullfilename
)
4990 changed_fields
[u
'filename'] = self
.rtnRelativePath(fullfilename
, u
'mythvideo')
4991 if changed_fields
[u
'filename'][0] != u
'/':
4992 changed_fields
[u
'host'] = localhostname
.lower()
4993 else: # Deal with the whole mixing Video SG and local with SG graphics mess
4994 for key
in graphicsDirectories
.keys():
4995 if key
== u
'screenshot' or not changed_fields
.has_key(key
):
4997 if changed_fields
[key
][0] == u
'/':
5000 changed_fields
.remove(key
)
5004 if len(changed_fields
):
5005 if self
.config
['simulation']:
5006 if program
['subtitle']:
5008 u
"Simulation MythTV DB update for Miro video (%s - %s)\n" % (program
['title'], program
['subtitle']))
5011 u
"Simulation MythTV DB update for Miro video (%s)\n" % (program
['title'],))
5013 Video(id=intid
, db
=mythvideo
).update(changed_fields
)
5014 # end updateMiroVideo()
5016 def _getScheduledRecordedProgramList(self
):
5017 '''Find all Scheduled and Recorded programs
5018 return array of found programs, if none then empty array is returned
5022 # Get pending recordings
5024 progs
= MythBE(backend
=mythbeconn
.hostname
, db
=mythbeconn
.db
).getUpcomingRecordings()
5025 except MythError
, e
:
5026 sys
.stderr
.write(u
"\n! Error: Getting Upcoming Recordings list: %s\n" % e
.args
[0])
5031 if prog
.title
== None:
5033 record
['title'] = prog
.title
5034 record
['subtitle'] = prog
.subtitle
5035 record
['seriesid'] = prog
.seriesid
5037 if record
['subtitle'] and prog
.airdate
!= None:
5038 record
['originalairdate'] = prog
.airdate
[:4]
5040 if prog
.year
!= '0':
5041 record
['originalairdate'] = prog
.year
5042 elif prog
.airdate
!= None:
5043 record
['originalairdate'] = prog
.airdate
[:4]
5044 for program
in programs
: # Skip duplicates
5045 if program
['title'] == record
['title']:
5048 programs
.append(record
)
5050 # Get recorded table field names:
5052 recordedlist
= MythBE(backend
=mythbeconn
.hostname
, db
=mythbeconn
.db
).getRecordings()
5053 except MythError
, e
:
5054 sys
.stderr
.write(u
"\n! Error: Getting recorded programs list: %s\n" % e
.args
[0])
5057 if not recordedlist
:
5060 recordedprogram
= {}
5061 for recordedProgram
in recordedlist
:
5063 recordedRecord
= recordedProgram
.getRecorded()
5064 except MythError
, e
:
5065 sys
.stderr
.write(u
"\n! Error: Getting recorded table record: %s\n" % e
.args
[0])
5067 if recordedRecord
.recgroup
== u
'Deleted':
5070 if recordedRecord
.title
== None:
5072 if recordedRecord
.chanid
== 9999:
5073 recorded
[u
'miro_tv'] = True
5074 recorded
[u
'title'] = recordedRecord
.title
5075 recorded
[u
'subtitle'] = recordedRecord
.subtitle
5076 recorded
[u
'seriesid'] = recordedRecord
.seriesid
5077 for program
in programs
: # Skip duplicates
5078 if program
['title'] == recorded
['title']:
5081 programs
.append(recorded
)
5082 # Get Release year for recorded movies
5083 # Get Recorded videos recordedprogram / airdate
5085 recordedDetails
= recordedRecord
.getRecordedProgram()
5086 except MythError
, e
:
5087 sys
.stderr
.write(u
"\n! Error: Getting recordedprogram table record: %s\n" % e
.args
[0])
5089 if not recordedDetails
:
5091 if not recordedDetails
.subtitle
:
5092 recordedprogram
[recordedDetails
.title
]= u
'%d' % recordedDetails
.airdate
5094 # Add release year to recorded movies
5095 for program
in programs
:
5096 if recordedprogram
.has_key(program
['title']):
5097 program
['originalairdate'] = recordedprogram
[program
['title']]
5100 # Add real names to mb_tv if they are among the recorded videos
5101 if len(self
.config
['mb_tv_channels']):
5102 for program
in programs
:
5103 programtitle
= filter(is_not_punct_char
, program
[u
'title'].lower())
5104 if programtitle
in self
.config
['mb_tv_channels'].keys():
5105 self
.config
['mb_tv_channels'][programtitle
][1] = program
[u
'title']
5107 # Check that each program has an original airdate
5108 for program
in programs
:
5109 if not program
.has_key('originalairdate'):
5110 program
['originalairdate'] = u
'0000' # Set the original airdate to zero (unknown)
5112 # If there are any Miro TV or movies to process then add them to the list
5113 if len(self
.config
['mb_tv_channels']) or len(self
.config
['mb_movies']):
5114 miromythvideorecs
= self
._getMiroVideometadataRecords
()
5115 if miromythvideorecs
:
5116 # Create array used to check for duplicates
5119 for program
in programs
:
5120 programtitle
= filter(is_not_punct_char
, program
[u
'title'].lower())
5121 if programtitle
in self
.config
['mb_tv_channels'].keys():
5122 if not program
[u
'title'] in duplicatekeys
:
5123 duplicatekeys
[program
[u
'title']] = i
5124 elif programtitle
in self
.config
['mb_movies'].keys():
5125 moviename
= program
['subtitle']
5129 index
= moviename
.find(self
.config
['mb_movies'][programtitle
])
5131 moviename
= moviename
[:index
].strip()
5132 if not moviename
in duplicatekeys
:
5133 duplicatekeys
[moviename
] = i
5136 for record
in miromythvideorecs
:
5138 program
[u
'title'] = record
[u
'title']
5139 program
[u
'subtitle'] = record
[u
'subtitle']
5140 program
[u
'originalairdate'] = record
[u
'year']
5141 recordtitle
= filter(is_not_punct_char
, record
[u
'title'].lower())
5142 if recordtitle
in self
.config
['mb_tv_channels'].keys():
5143 if not record
[u
'title'] in duplicatekeys
.keys():
5144 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'tv')
5145 duplicatekeys
[program
[u
'title']] = len(programs
)
5146 programs
.append(program
)
5147 self
.config
['mb_tv_channels'][recordtitle
][1] = record
[u
'title']
5148 elif programs
[duplicatekeys
[program
[u
'title']]].has_key(u
'miro'):
5149 programs
[duplicatekeys
[program
[u
'title']]][u
'miro'][u
'intid'].append(record
[u
'intid'])
5151 programs
[duplicatekeys
[program
[u
'title']]][u
'miro'] = self
._getExtraMiroDetails
(record
, u
'tv')
5152 elif recordtitle
in self
.config
['mb_movies'].keys():
5153 moviename
= record
['subtitle']
5157 index
= moviename
.find(self
.config
['mb_movies'][filter(is_not_punct_char
, program
[u
'title'].lower())])
5159 moviename
= moviename
[:index
].strip()
5160 if not moviename
in duplicatekeys
.keys():
5161 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5162 if program
[u
'miro'][u
'inetref']:
5163 duplicatekeys
[moviename
] = len(programs
)
5164 programs
.append(program
)
5165 elif programs
[duplicatekeys
[moviename
]].has_key(u
'miro'):
5166 programs
[duplicatekeys
[moviename
]][u
'miro'][u
'intid'].append(record
[u
'intid'])
5168 program
[u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5169 if program
[u
'miro'][u
'inetref']:
5170 programs
[duplicatekeys
[moviename
]][u
'miro'] = self
._getExtraMiroDetails
(record
, u
'movies')
5172 # Check that each program has seriesid
5173 for program
in programs
:
5174 if not program
.has_key('seriesid'):
5175 program
['seriesid'] = u
'' # Set an empty seriesid - Generall only for Miro Videos
5176 if program
['seriesid'] == None:
5177 program
['seriesid'] = u
'' # Set an empty seriesid
5180 # end _getScheduledRecordedProgramList
5183 def _getScheduledRecordedTVGraphics(self
, program
, graphics_type
):
5184 '''Get TV show graphics for Scheduled and Recorded TV programs
5185 return None if no graphics found
5186 return fullpath and filename of downloaded graphics file
5188 if graphics_type
== 'coverfile':
5189 graphics_type
= 'poster'
5191 self
.config
['sid'] = None
5192 if self
.config
['series_name_override']:
5193 if self
.config
['series_name_override'].has_key(program
['title'].lower()):
5194 self
.config
['sid'] = self
.config
['series_name_override'][program
['title'].lower()]
5195 # Find out if there are any Series level graphics available
5196 self
.config
['toprated'] = True
5197 self
.config
['episode_name'] = None
5198 self
.config
['series_name'] = program
['title']
5199 self
.config
['season_num'] = None
5200 self
.config
['episode_num'] = None
5202 series_graphics
= self
.getGraphics(graphics_type
)
5204 if series_graphics
!= None:
5205 cfile
= { 'file_seriesname': program
['title'],
5206 'inetref': self
.config
['sid'],
5207 'seasno': self
.config
['season_num'],
5208 'epno': self
.config
['episode_num'],
5210 'filename': program
['title'],
5214 return self
._getTvdbGraphics
(cfile
, graphics_type
, toprated
=True, watched
=True)
5216 # end _getScheduledRecordedTVGraphics
5218 def _downloadScheduledRecordedGraphics(self
):
5219 '''Get Scheduled and Recorded programs and Miro vidoes get their graphics if not already
5221 return (nothing is returned)
5223 global localhostname
5225 # Initialize reporting stats
5226 total_progs_checked
= 0
5227 total_posters_found
= 0
5228 total_banners_found
= 0
5229 total_fanart_found
= 0
5230 total_posters_downloaded
= 0
5231 total_banners_downloaded
= 0
5232 total_fanart_downloaded
= 0
5234 total_miro_movies
= 0
5236 programs
= self
._getScheduledRecordedProgramList
()
5238 if not len(programs
): # Is there any programs to process?
5241 # Add any Miro Bridge mb_tv dictionary items to 'series_name_override' dictionary
5242 if not self
.config
['series_name_override'] and len(self
.config
['mb_tv_channels']):
5243 self
.config
['series_name_override'] = {}
5244 for miro_tv_key
in self
.config
['mb_tv_channels'].keys():
5245 if self
.config
['mb_tv_channels'][miro_tv_key
][0]:
5246 self
.config
['series_name_override'][self
.config
['mb_tv_channels'][miro_tv_key
][1].lower()] = self
.config
['mb_tv_channels'][miro_tv_key
][0]
5248 total_progs_checked
= len(programs
)
5250 # Get totals of Miro TV shows and movies that will be processed
5251 for program
in programs
:
5252 if program
.has_key(u
'miro'):
5253 if not program
[u
'miro'][u
'tv']:
5254 total_miro_movies
+=1
5257 elif program
.has_key(u
'miro_tv'):
5258 if filter(is_not_punct_char
, program
[u
'title'].lower()) in self
.config
['mb_movies'].keys():
5259 total_miro_movies
+=1
5263 # Prossess all TV shows and Movies
5264 for program
in programs
:
5265 program
['need'] = False # Initalize that this program does not need graphic(s) downloaded
5267 program_override_tv
= False
5268 # Check if a subtitle-less program is really a TV show with an override. This compensates for
5269 # poor EPG data sources (as has been reported from at least Australia)
5270 if not program
['subtitle'] and program
['title'].lower() in self
.config
['series_name_override']:
5272 result
= self
._searchforSeries
(program
['title'])
5273 program_override_tv
= True
5274 except Exception, e
:
5277 # Even movies get the ' Season' added to the image names so that movie such as '1408' do not clash
5278 # with TMDB#ed image names
5279 pattern
= u
'%s Season*.*'
5280 if not program
.has_key(u
'miro'):
5281 if program
['subtitle'] or program_override_tv
:
5282 graphics_name
= program
['title']
5284 if not int(program
['originalairdate']):
5285 graphics_name
= program
['title']
5287 graphics_name
= "%s (%s)" % (program
['title'], program
['originalairdate'])
5289 mirodetails
= program
[u
'miro']
5290 if mirodetails
[u
'tv']:
5291 graphics_name
= program
['title']
5293 graphics_name
= mirodetails
[u
'inetref']
5295 self
.absolutepath
= False # All Scheduled Recorded and Miro videos start in the SG "Default"
5297 # Search for graphics that are already downloaded
5298 for directory
in graphicsDirectories
.keys():
5299 if directory
== 'screenshot': # There is no downloading of screenshots required
5300 program
[directory
] = True
5302 if directory
== 'banner' and not program
['subtitle']: # No banners for movies
5303 program
[directory
] = True
5306 if not mirodetails
[u
'tv'] and directory
== 'banner': # No banners for movies
5307 program
[directory
] = True
5310 filename
= program
['title']
5311 elif mirodetails
[u
'tv']:
5312 filename
= program
['title']
5314 filename
= mirodetails
[u
'inetref']
5316 # Deal with TV series names that would generate invalid file names for images TV and movies
5317 self
.program_seriesid
= None
5318 if not isValidPosixFilename(filename
) and program
['seriesid'] != u
'':
5319 filename
= program
['seriesid']
5320 self
.program_seriesid
= program
['seriesid']
5322 # Actual check for existing graphics
5323 for dirct
in self
.config
[graphicsDirectories
[directory
]]:
5325 dir_list
= os
.listdir(unicode(dirct
, 'utf8'))
5326 except (UnicodeEncodeError, TypeError):
5327 dir_list
= os
.listdir(dirct
)
5328 for flenme
in dir_list
:
5329 if fnmatch
.fnmatch(flenme
.lower(), (pattern
% filename
).lower()):
5330 program
[directory
] = True
5331 if directory
== 'coverfile':
5332 total_posters_found
+=1
5333 elif directory
== 'banner':
5334 total_banners_found
+=1
5336 total_fanart_found
+=1
5337 if mirodetails
: # Update the Miro MythVideo records with any existing graphics
5338 mirodetails
[directory
] = self
.rtnRelativePath(u
'%s/%s' % (dirct
, flenme
), directory
)
5344 program
['need'] = True
5345 program
[directory
] = False
5347 # Check if there are any graphics to download
5348 if not program
['need']:
5350 filename
= program
['title']
5351 elif mirodetails
[u
'tv']:
5352 filename
= program
['title']
5354 filename
= mirodetails
[u
'moviename']
5355 self
._displayMessage
("All Graphics already downloaded for [%s]" % filename
)
5356 if mirodetails
: # Update the Miro MythVideo records with any new graphics
5357 self
.updateMiroVideo(program
)
5361 # It is more efficient to find inetref of movie once
5362 if not program
['subtitle'] and not program_override_tv
:
5363 if not program
.has_key('inetref'): # Was the inetref number already found?
5364 inetref
= self
._getTmdbIMDB
(graphics_name
, watched
=True)
5366 self
._displayMessage
("No movie inetref [%s]" % graphics_name
)
5367 # Fake subtitle as this may be a TV series without a subtitle
5368 program
['subtitle']=' '
5370 self
._displayMessage
("Found movie inetref (%s),[%s]" % (inetref
, graphics_name
))
5371 program
['inetref'] = inetref
5372 elif not mirodetails
[u
'tv']:
5373 program
['inetref'] = mirodetails
[u
'inetref']
5375 # Download missing graphics
5376 for key
in graphicsDirectories
.keys():
5377 if program
[key
]: # Check if this type of graphic is already downloaded
5379 miromovieflag
= False
5381 if not mirodetails
[u
'tv']:
5382 miromovieflag
= True
5383 # This is a TV episode or Miro TV show
5384 if (program
['subtitle'] or program_override_tv
) and not miromovieflag
:
5385 results
= self
._getScheduledRecordedTVGraphics
(program
, key
)
5388 filename
= program
['title']
5389 elif mirodetails
[u
'tv']:
5390 filename
= program
['title']
5392 filename
= mirodetails
[u
'moviename']
5393 if key
== 'coverfile':
5394 total_posters_downloaded
+=1
5395 elif key
== 'banner':
5396 total_banners_downloaded
+=1
5397 elif key
== 'fanart':
5398 total_fanart_downloaded
+=1
5399 if mirodetails
: # Save the filename for storing later
5400 mirodetails
[key
] = results
5402 self
._displayMessage
("TV Series - No (%s) for [%s]" % (key
, program
['title']))
5403 else: # This is a movie
5404 title
= program
['title']
5405 filename
= program
['title']
5407 title
= mirodetails
[u
'inetref']
5408 filename
= mirodetails
[u
'inetref']
5409 cfile
= { 'file_seriesname': title
,
5410 'inetref': program
['inetref'],
5414 'filename': filename
,
5418 if key
== 'coverfile':
5422 results
= self
._getTmdbGraphics
(cfile
, g_type
, watched
=True)
5424 results
= self
._getSecondarySourceGraphics
(cfile
, key
, watched
=True)
5426 if key
== 'coverfile':
5427 total_posters_downloaded
+=1
5428 elif key
== 'fanart':
5429 total_fanart_downloaded
+=1
5430 if mirodetails
: # Save the filename for storing later
5431 mirodetails
[key
] = results
5434 filename
= program
['title']
5436 filename
= mirodetails
[u
'moviename']
5437 self
._displayMessage
("No (%s) for [%s]" % (key
, filename
))
5439 if mirodetails
: # Update the Miro MythVideo records with any new graphics
5440 self
.updateMiroVideo(program
)
5443 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
))
5446 sys
.stdout
.write(u
'\n-------------Scheduled & Recorded----------\n')
5447 for program
in programs
:
5448 if not program
.has_key(u
'miro'):
5449 if program
.has_key(u
'miro_tv'):
5450 if filter(is_not_punct_char
, program
[u
'title'].lower()) in self
.config
['mb_movies'].keys():
5451 sys
.stdout
.write(u
'Miro Movie Trailer: %s\n' % (program
['title'], ))
5453 sys
.stdout
.write(u
'Miro TV Show: %s\n' % (program
['title'], ))
5455 if program
['subtitle']:
5456 sys
.stdout
.write(u
'%s\n' % (program
['title'], ))
5458 if program
['originalairdate'] != u
'0000':
5459 sys
.stdout
.write(u
'%s\n' % ("%s (%s)" % (program
['title'], program
['originalairdate'])))
5461 sys
.stdout
.write(u
'%s\n' % (program
['title'], ))
5462 elif program
[u
'miro'][u
'tv']:
5463 sys
.stdout
.write(u
'Miro TV Show: %s\n' % (program
['title'], ))
5465 sys
.stdout
.write(u
'Miro Movie Trailer: %s\n' % (program
[u
'miro'][u
'moviename'], ))
5467 # end _downloadScheduledRecordedGraphics()
5470 def findFileInDir(self
, filename
, directories
, suffix
=None, fuzzy_match
=False):
5471 '''Find if a file is in any of the specified directories. An exact match or a variation.
5472 return False - File not found in directories
5473 return True - Absolute file name and path
5475 (dirName
, fileName
) = os
.path
.split(filename
)
5476 (fileBaseName
, fileExtension
) = os
.path
.splitext(fileName
)
5477 if fuzzy_match
: # Match even when the names are not exactly the same by removing punctuation
5478 for dirct
in directories
:
5480 dir_list
= os
.listdir(unicode(dirct
, 'utf8'))
5481 except (UnicodeEncodeError, TypeError):
5482 dir_list
= os
.listdir(dirct
)
5484 for file_name
in dir_list
:
5485 match_list
.append(filter(is_not_punct_char
, file_name
.lower()))
5487 if fileBaseName
.find(suffix
) == -1:
5488 file_path
= filter(is_not_punct_char
, (u
"%s%s%s" % (fileBaseName
, suffix
, fileExtension
)).lower())
5489 file_path2
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5491 file_path
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5492 file_path2
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
.replace(suffix
, u
''), fileExtension
)).lower())
5493 if file_path
in match_list
:
5494 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path
)])
5495 if file_path2
in match_list
:
5496 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path2
)])
5499 file_path
= filter(is_not_punct_char
, (u
"%s%s" % (fileBaseName
, fileExtension
)).lower())
5500 if file_path
in match_list
:
5501 return u
'%s/%s' % (dirct
, dir_list
[match_list
.index(file_path
)])
5504 else: # Find an exact match
5505 for directory
in directories
:
5506 if filename
[0] != u
'/' and dirName
!= u
'':
5507 dir_name
= u
"%s/%s" % (directory
, dirName
)
5509 dir_name
= directory
5511 if fileBaseName
.find(suffix
) == -1:
5512 file_path
= u
"%s/%s%s%s" % (dir_name
, fileBaseName
, suffix
, fileExtension
)
5513 file_path2
= u
'%s/%s' % (dir_name
, fileName
)
5515 file_path
= u
'%s/%s' % (dir_name
, fileName
)
5516 file_path2
= u
'%s/%s' % (dir_name
, fileName
.replace(suffix
, u
''))
5517 if os
.path
.isfile(file_path
):
5519 if os
.path
.isfile(file_path2
):
5523 file_path
= u
'%s/%s' % (dir_name
, fileName
)
5524 if os
.path
.isfile(file_path
):
5528 # end findFileInDir()
5532 num_secondary_source_graphics_downloaded
=0
5533 num_secondary_source_metadata_downloaded
=0
5535 def processMythTvMetaData(self
):
5536 '''Check each video file in the mythvideo directories download graphics files and meta data then
5537 update MythTV data base meta data with any new information.
5539 # Verify that the proper fields are present
5540 db_version
= mythdb
.settings
.NULL
.DBSchemaVer
5541 field_names
= mythvideo
.tablefields
['videometadata']
5542 for field
in ['season', 'episode', 'coverfile', 'screenshot', 'banner', 'fanart']:
5543 if not field
in field_names
:
5544 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
))
5547 # Initailize and instance to the TMDB api
5548 apikey
= "c27cb71cff5bd76e1a7a009380562c62"
5549 if self
.config
['interactive']:
5550 # themoviedb.org api key given by Travis Bell for Mythtv
5551 self
.config
['tmdb_api'] = tmdb_api
.MovieDb(apikey
,
5554 select_first
= False,
5555 debug
= self
.config
['debug_enabled'],
5557 language
= self
.config
['local_language'],
5558 search_all_languages
= True,)
5560 self
.config
['tmdb_api'] = tmdb_api
.MovieDb(apikey
,
5562 interactive
= False,
5563 select_first
= False,
5564 debug
= self
.config
['debug_enabled'],
5565 language
= self
.config
['local_language'],
5566 search_all_languages
= True,)
5568 # If there were directories specified move them and update the MythTV db meta data accordingly
5569 if self
.config
['video_dir']:
5570 if len(self
.config
['video_dir']) % 2 == 0:
5571 validFiles
= self
._moveVideoFiles
(self
.config
['video_dir'])
5572 self
.config
[u
'file_move_flag'] = False
5574 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']))
5577 # Check if only missing inetref video's should be processed
5578 if self
.config
['mythtv_inetref'] or self
.config
['mythtv_ref_num']:
5579 validFiles
= self
._findMissingInetref
()
5580 if validFiles
== None:
5581 sys
.stderr
.write(u
"\n! Warning: There were no missing interef video files found.\n\n")
5583 elif not len(validFiles
):
5584 sys
.stderr
.write(u
"\n! Warning: There were no missing interef video files found.\n\n")
5587 # Check if this is a Scheduled and Recorded graphics download request
5588 if self
.config
['mythtv_watched']:
5589 self
._downloadScheduledRecordedGraphics
()
5592 # Check if this is just a Janitor (clean up unused graphics files) request
5593 if self
.config
['mythtvjanitor']:
5594 self
._graphicsCleanup
()
5597 directories
=self
.config
['mythvideo']
5599 if not len(directories
):
5600 sys
.stderr
.write(u
"\n! Error: There must be a video directory specified in MythTv\n")
5605 num_fanart_downloads
=0
5606 num_posters_downloads
=0
5607 num_banners_downloads
=0
5608 num_episode_metadata_downloads
=0
5609 num_movies_using_imdb_numbers
=0
5610 num_symlinks_created
=0
5611 num_mythdb_updates
=0
5612 num_posters_below_min_size
=0
5613 videos_with_small_posters
=[]
5614 videos_using_imdb_numbers
=[]
5615 videos_updated_metadata
=[]
5618 sys
.stdout
.write(u
'Mythtv video database maintenance start: %s\n' % (datetime
.datetime
.now()).strftime("%Y-%m-%d %H:%M"))
5620 if not self
.config
['video_dir'] and not self
.config
['mythtv_inetref'] and not self
.config
['mythtv_ref_num']:
5621 allFiles
= self
._findFiles
(directories
, self
.config
['recursive'] , verbose
= self
.config
['debug_enabled'])
5622 validFiles
= self
._processNames
(allFiles
, verbose
= self
.config
['debug_enabled'], movies
=True)
5624 if not len(validFiles
):
5625 sys
.stderr
.write(u
"\n! Error: No valid video files found\n")
5628 tv_series_season_format
=u
"%s/%s Season %d.%s"
5629 tv_series_format
=u
"%s/%s.%s"
5630 for cfile
in validFiles
:
5631 self
._displayMessage
(u
"\nNow processing video file (%s)(%s)(%s)\n" % (cfile
['filename'], cfile
['seasno'], cfile
['epno']))
5634 videopath
= tv_series_format
% (cfile
['filepath'], cfile
['filename'], cfile
['ext'])
5635 # Find the MythTV meta data
5636 result
= mythvideo
.getVideo(exactfile
=videopath
)
5640 intid
= result
.intid
5642 result
= mythvideo
.getVideo(exactfile
=self
.rtnRelativePath(videopath
, u
'mythvideo'), host
=localhostname
.lower())
5645 has_metadata
= False
5647 intid
= result
.intid
5648 if result
.category
== 'none' and result
.year
== 1895:
5649 has_metadata
= False
5653 if result
.category
== 'none' and result
.year
== 1895:
5654 has_metadata
= False
5659 # Unless explicitly requested with options -MI or -MG do not add missing videos to DB
5660 if not self
.config
['interactive'] and not self
.config
['mythtv_guess']:
5662 # Create a new empty entry
5663 sys
.stdout
.write(u
"\n\nEntry does not exist in MythDB. Adding (%s).\n" % cfile
['filename'])
5664 new_rec
= {'title': cfile['file_seriesname'], 'filename': self.rtnRelativePath(videopath, u'mythvideo')}
5665 videopath
= self
.rtnRelativePath(videopath
, u
'mythvideo')
5666 if videopath
[0] == '/':
5667 intid
= Video(db
=mythvideo
).create(new_rec
).intid
5669 new_rec
['host'] = localhostname
.lower()
5670 intid
= Video(db
=mythvideo
).create(new_rec
).intid
5671 elif not has_metadata
:
5672 sys
.stdout
.write(u
"\n\nEntry exists in MythDB but category is 0 and year is 1895 (default values).\nUpdating (%s).\n" % cfile
['filename'])
5673 filename
= self
.rtnRelativePath(videopath
, u
'mythvideo')
5674 if filename
[0] == u
'/':
5675 Video(id=intid
, db
=mythvideo
).update({'filename': filename, u'host': u''}
)
5677 Video(id=intid
, db
=mythvideo
).update({'filename': filename, u'host': localhostname.lower()}
)
5678 if cfile
['seasno'] == 0 and cfile
['epno'] == 0:
5683 # Get a dictionary of the existing meta data plus a copy for update comparison
5685 vim
= Video(id=intid
, db
=mythvideo
)
5686 for key
in vim
.keys():
5687 meta_dict
[key
] = vim
[key
]
5689 # Fix a metadata record that has an incorrectly initialized inetref number value
5690 if meta_dict
['inetref'] == None:
5691 meta_dict
['inetref'] = u
'00000000'
5692 available_metadata
= dict(meta_dict
)
5694 available_metadata
['season']=cfile
['seasno']
5695 available_metadata
['episode']=cfile
['epno']
5697 if available_metadata
['title'] == u
'':
5698 available_metadata
['title'] = cfile
['file_seriesname']
5700 # Set whether a video file is stored in a Storage Group or not
5701 if available_metadata
['filename'][0] == u
'/':
5702 self
.absolutepath
= True
5704 self
.absolutepath
= False
5706 # There must be an Internet reference number. Get one for new records.
5707 if _can_int(meta_dict
['inetref']) and not meta_dict
['inetref'] == u
'00000000' and not meta_dict
['inetref'] == '':
5708 if meta_dict
['inetref'] == '99999999': # Records that are not updated by Jamu
5710 inetref
= meta_dict
['inetref']
5711 cfile
['inetref'] = meta_dict
['inetref']
5714 if not self
.config
['interactive'] and not self
.config
['mythtv_guess']:
5715 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']))
5717 inetref
= self
._getTmdbIMDB
(available_metadata
['title'])
5718 cfile
['inetref'] = inetref
5720 self
._displayMessage
(u
"themoviedb.com does not recognize the movie (%s) - Cannot update metadata - skipping\n" % available_metadata
['title'])
5721 missing_inetref
.append(available_metadata
['title'])
5723 # Only update the reference number
5724 if self
.config
['mythtv_ref_num'] or inetref
== '99999999':
5725 Video(id=intid
, db
=mythvideo
).update({'inetref': inetref}
)
5726 num_mythdb_updates
+=1
5727 videos_updated_metadata
.append(cfile
['filename'])
5728 self
._displayMessage
(u
"\nReference number (%s) added for (%s) \n" % (inetref
, cfile
['filename']))
5732 for key
in available_metadata
.keys():
5733 copy
[key
] = available_metadata
[key
]
5734 tmp_dict
= self
._getTvdbMetadata
(cfile
, copy
)
5736 self
._displayMessage
(u
"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile
['seasno'], cfile
['epno'], videopath
))
5737 missing_inetref
.append(available_metadata
['title'])
5739 inetref
= tmp_dict
['inetref']
5740 available_metadata
['title'] = tmp_dict
['title']
5741 cfile
['file_seriesname'] = tmp_dict
['title']
5742 # Only update the reference number and title
5743 if self
.config
['mythtv_ref_num'] or inetref
== '99999999':
5744 if inetref
== u
'99999999':
5745 Video(id=intid
, db
=mythvideo
).update({'inetref': inetref}
)
5747 Video(id=intid
, db
=mythvideo
).update({'inetref': inetref, 'title': tmp_dict['title']}
)
5748 num_mythdb_updates
+=1
5749 videos_updated_metadata
.append(cfile
['filename'])
5750 self
._displayMessage
(u
"\nReference number (%s) added for (%s) \n" % (inetref
, cfile
['filename']))
5752 cfile
['inetref'] = inetref
5753 available_metadata
['inetref'] = inetref
5755 if (meta_dict
['subtitle'] == None or meta_dict
['subtitle'] == '') and not movie
:
5756 tmp_subtitle
= self
._getSubtitle
(cfile
)
5757 if tmp_subtitle
== None:
5758 self
._displayMessage
(u
"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile
['seasno'], cfile
['epno'], videopath
))
5761 available_metadata
['subtitle'] = tmp_subtitle
5762 available_metadata
['title'] = self
.config
['series_name']
5763 cfile
['file_seriesname'] = self
.config
['series_name']
5765 # Check if current inetref is a IMDB#
5766 # If so then check it can be changed to tmdb#
5767 # If it can be changed then rename any graphics and update meta data
5768 if movie
and len(inetref
) == 7:
5769 self
._displayMessage
(u
"%s has IMDB# (%s)" % (available_metadata
['title'], inetref
))
5770 num_movies_using_imdb_numbers
+=1
5771 videos_using_imdb_numbers
.append(u
"%s has IMDB# (%s)" % (available_metadata
['title'], inetref
))
5772 movie_data
= self
._getTmdbMetadata
(cfile
, dict(available_metadata
))
5773 if movie_data
.has_key('inetref'):
5774 if available_metadata
['inetref'] != movie_data
['inetref']:
5775 available_metadata
['inetref'] = movie_data
['inetref']
5776 inetref
= movie_data
['inetref']
5777 cfile
['inetref'] = movie_data
['inetref']
5778 for graphic_type
in ['coverfile', 'banner', 'fanart']: # Rename graphics files
5779 if available_metadata
[graphic_type
] == None or available_metadata
[graphic_type
] == '':
5781 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5782 if os
.path
.isfile(graphic_file
):
5783 filepath
, filename
= os
.path
.split(graphic_file
)
5784 filename
, ext
= os
.path
.splitext( filename
)
5786 if self
.config
['simulation']:
5788 u
"Simulation renaming (%s) to (%s)\n" % (graphic_file
, tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
))
5791 dest
= tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
)
5793 if not os
.path
.isfile(dest
):
5794 os
.rename(graphic_file
, dest
)
5797 u
"Renaming image file (%s) to (%s) failed, error(%s)\n" % (graphic_file
, dest
, e
))
5799 self
._displayMessage
(u
"Renamed (%s) to (%s)\n" % (graphic_file
, tv_series_format
% (filepath
, inetref
+self
.graphic_suffix
[graphic_type
], ext
)))
5800 available_metadata
[graphic_type
]= self
.rtnRelativePath(dest
, graphicsDirectories
[graphic_type
])
5802 ###############################################################################
5803 # START of metadata Graphics logic - Checking, downloading, renaming
5804 ###############################################################################
5805 for graphic_type
in ['coverfile', 'banner', 'fanart']:
5806 ###############################################################################
5807 # START of MOVIE graphics updating
5808 ###############################################################################
5809 # Check that there are local graphics path for abs path video
5810 # An abs path video can only use the FE specified graphic directories
5811 if self
.absolutepath
:
5812 if not len(self
.config
['localpaths'][graphicsDirectories
[graphic_type
]]):
5814 graphicsdirs
= self
.config
['localpaths'][graphicsDirectories
[graphic_type
]]
5816 graphicsdirs
= self
.config
[graphicsDirectories
[graphic_type
]]
5818 if graphic_type
== 'banner':
5820 if graphic_type
== 'coverfile':
5825 undersized_graphic
= False
5826 for ext
in self
.image_extensions
:
5827 for graphicsdir
in graphicsdirs
:
5828 filename
= self
.findFileInDir(u
"%s.%s" % (inetref
, ext
), [graphicsdir
], suffix
=self
.graphic_suffix
[graphic_type
])
5831 available_metadata
[graphic_type
]=self
.rtnRelativePath(filename
, graphicsDirectories
[graphic_type
])
5832 if graphic_type
== 'coverfile':
5834 (width
, height
) = self
.config
['image_library'].open(filename
).size
5835 if width
< self
.config
['min_poster_size']:
5836 num_posters_below_min_size
+=1
5837 videos_with_small_posters
.append(cfile
['filename'])
5838 undersized_graphic
= True
5841 undersized_graphic
= True
5843 need_graphic
= False
5845 if not need_graphic
:
5848 if need_graphic
== True:
5849 dummy_graphic
= self
._getTmdbGraphics
(cfile
, g_type
)
5851 # Try secondary source if themoviedb.com did not have graphicrecord['title']
5852 if dummy_graphic
== None or undersized_graphic
== True:
5853 dummy_graphic
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5855 if dummy_graphic
!= None:
5856 available_metadata
[graphic_type
] = self
.rtnRelativePath(dummy_graphic
, graphicsDirectories
[graphic_type
])
5857 if graphic_type
== 'fanart':
5858 self
._displayMessage
(u
"Movie - Added fan art for(%s)" % cfile
['filename'])
5859 num_fanart_downloads
+=1
5861 self
._displayMessage
(u
"Movie - Added a poster for(%s)" % cfile
['filename'])
5862 num_posters_downloads
+=1
5864 # END of Movie graphics updates ###############################################
5866 ###############################################################################
5867 # START of TV Series graphics updating
5868 ###############################################################################
5869 need_graphic
= False
5870 new_format
= True # Initialize that a graphics file NEEDS a new format
5871 # Check if an existing TV series graphic file is in the old naming format
5872 if available_metadata
[graphic_type
] != None and available_metadata
[graphic_type
] != 'No Cover' and available_metadata
[graphic_type
] != '':
5873 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5874 filepath
, filename
= os
.path
.split(graphic_file
)
5875 filename
, ext
= os
.path
.splitext( filename
)
5876 if filename
.find(u
' Season ') != -1:
5880 if need_graphic
or new_format
: # Graphic does not exist or is in an old format
5881 for ext
in self
.image_extensions
:
5882 for graphicsdir
in graphicsdirs
:
5883 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)
5885 available_metadata
[graphic_type
]=self
.rtnRelativePath(filename
, graphicsDirectories
[graphic_type
])
5886 need_graphic
= False
5887 if graphic_type
== 'coverfile':
5889 (width
, height
) = self
.config
['image_library'].open(filename
).size
5890 if width
< self
.config
['min_poster_size']:
5891 num_posters_below_min_size
+=1
5892 videos_with_small_posters
.append(cfile
['filename'])
5895 undersized_graphic
= True
5898 if not need_graphic
:
5901 graphic_file
= self
.rtnAbsolutePath(available_metadata
[graphic_type
], graphicsDirectories
[graphic_type
])
5902 if not graphic_file
== None:
5903 graphic_file
= self
.findFileInDir(graphic_file
, [graphicsdir
], suffix
=self
.graphic_suffix
[graphic_type
], fuzzy_match
=True)
5904 if graphic_file
== None:
5906 if not need_graphic
: # Have graphic but may be using an old naming convention
5908 season_missing
= False
5909 suffix_missing
= False
5910 if graphic_file
.find(u
' Season ') == -1: # Check for Season
5912 season_missing
= True
5913 if graphic_file
.find(self
.graphic_suffix
[graphic_type
]) == -1:
5915 suffix_missing
= True
5917 filepath
, filename
= os
.path
.split(graphic_file
)
5918 baseFilename
, ext
= os
.path
.splitext( filename
)
5919 baseFilename
= self
.sanitiseFileName(baseFilename
)
5920 if season_missing
and suffix_missing
:
5921 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
, available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5922 elif suffix_missing
:
5923 newFilename
= u
"%s/%s%s%s" % (filepath
, baseFilename
, self
.graphic_suffix
[graphic_type
], ext
)
5924 elif season_missing
:
5925 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5926 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5927 if self
.config
['simulation']:
5929 u
"Simulation renaming (%s) to (%s)\n" % (graphic_file
, newFilename
)
5932 os
.rename(graphic_file
, newFilename
)
5933 available_metadata
[graphic_type
]= self
.rtnRelativePath(newFilename
, graphicsDirectories
[graphic_type
])
5935 available_metadata
[graphic_type
]= self
.rtnRelativePath(graphic_file
, graphicsDirectories
[graphic_type
])
5936 else: # Must see if a graphic is on thetvdb wiki
5937 if graphic_type
== 'coverfile' or graphic_type
== 'banner':
5938 available_metadata
[graphic_type
] = self
.rtnRelativePath(self
._getTvdbGraphics
(cfile
, graphic_type
), graphicsDirectories
[graphic_type
])
5939 if available_metadata
[graphic_type
] == None:
5940 tmp
= self
._getTvdbGraphics
(cfile
, graphic_type
, toprated
=True)
5942 tmp_fullfilename
= self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
])
5943 filepath
, filename
= os
.path
.split(tmp_fullfilename
)
5944 baseFilename
, ext
= os
.path
.splitext( filename
)
5945 baseFilename
= self
.sanitiseFileName(baseFilename
)
5946 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5947 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5948 if self
.config
['simulation']:
5950 u
"Simulation rename (%s) to (%s)\n" % (tmp_fullfilename
,newFilename
)
5953 self
._displayMessage
(u
"Rename existing graphic %s for series (%s)" % (graphic_type
, available_metadata
['title']))
5955 os
.rename(tmp_fullfilename
, newFilename
)
5956 if graphic_type
== 'coverfile':
5957 self
._displayMessage
("1-Added a poster for(%s)" % cfile
['filename'])
5958 num_posters_downloads
+=1
5960 self
._displayMessage
("1-Added a banner for(%s)" % cfile
['filename'])
5961 num_banners_downloads
+=1
5962 available_metadata
[graphic_type
] = self
.rtnRelativePath(newFilename
, graphicsDirectories
[graphic_type
])
5965 u
"IOError coping (%s) to (%s)\nError:(%s)\n" % (tmp_fullfilename
, newFilename
, e
))
5966 else: # Try a secondary source
5967 dummy
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5969 if graphic_type
== 'coverfile':
5970 self
._displayMessage
(u
"1-Secondary source poster for(%s)" % cfile
['filename'])
5971 num_posters_downloads
+=1
5973 self
._displayMessage
(u
"1-Secondary source banner for(%s)" % cfile
['filename'])
5974 num_banners_downloads
+=1
5975 available_metadata
[graphic_type
] = self
.rtnRelativePath(dummy
, graphicsDirectories
[graphic_type
])
5976 else: # download fanart
5977 tmp
= self
.rtnAbsolutePath(self
._getTvdbGraphics
(cfile
, graphic_type
, toprated
=True), graphicsDirectories
['fanart'])
5979 filepath
, filename
= os
.path
.split(tmp
)
5980 baseFilename
, ext
= os
.path
.splitext( filename
)
5981 baseFilename
= self
.sanitiseFileName(baseFilename
)
5982 baseFilename
= baseFilename
.replace(self
.graphic_suffix
[graphic_type
], u
'')
5983 newFilename
= u
"%s/%s Season %d%s%s" % (filepath
, baseFilename
.replace(u
' Season X', u
''), available_metadata
['season'], self
.graphic_suffix
[graphic_type
], ext
)
5984 if self
.config
['simulation']:
5986 u
"Simulation fanart rename (%s) to (%s)\n" % (tmp
, newFilename
)
5990 os
.rename(self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
]), newFilename
)
5991 available_metadata
['fanart'] = self
.rtnRelativePath(newFilename
, graphicsDirectories
['fanart'])
5992 num_fanart_downloads
+=1
5995 u
"IOError coping (%s) to (%s)\nError:(%s)\n" % (self
.rtnAbsolutePath(tmp
, graphicsDirectories
[graphic_type
]), newFilename
, e
))
5996 else: # Try a secondary source
5997 dummy
= self
._getSecondarySourceGraphics
(cfile
, graphic_type
)
5999 available_metadata
['fanart'] = self
.rtnRelativePath(dummy
, graphicsDirectories
['fanart'])
6000 num_fanart_downloads
+=1
6001 # END of TV Series graphics updating
6002 ###############################################################################
6003 # END of metadata Graphics logic - Checking, downloading, renaming
6004 ###############################################################################
6006 ###############################################################################
6007 # START of metadata text logic - Checking, downloading, renaming
6008 ###############################################################################
6009 # Clean up meta data code
6011 if available_metadata
['rating'] == 'TV Show':
6012 available_metadata
['rating'] = 'NR'
6014 # Check if any meta data needs updating
6015 metadata_update
= True
6016 for key
in available_metadata
.keys():
6017 if key
in self
.config
['metadata_exclude_as_update_trigger']:
6020 if key
== 'rating' and (available_metadata
[key
] == 'NR' or available_metadata
[key
] == '' or available_metadata
[key
] == 'Unknown'):
6021 self
._displayMessage
(
6022 u
"At least (%s) needs updating\n" % (key
))
6024 if key
== 'userrating' and available_metadata
[key
] == 0.0:
6025 self
._displayMessage
(
6026 u
"At least (%s) needs updating\n" % (key
))
6028 if key
== 'length' and available_metadata
[key
] == 0:
6029 self
._displayMessage
(
6030 u
"At least (%s) needs updating\n" % (key
))
6032 if key
== 'category' and available_metadata
[key
] == 0:
6033 self
._displayMessage
(
6034 u
"At least (%s) needs updating\n" % (key
))
6036 if key
== 'year' and (available_metadata
[key
] == 0 or available_metadata
[key
] == 1895):
6037 self
._displayMessage
(
6038 u
"At least (%s) needs updating\n" % (key
))
6040 if movie
and key
== 'subtitle': # There are no subtitles in movies
6042 if movie
and key
== 'plot' and available_metadata
[key
] != None:
6043 if len(available_metadata
[key
].split(' ')) < 10:
6044 self
._displayMessage
(
6045 u
"The plot is less than 10 words check if a better plot exists\n")
6047 if key
== 'releasedate' and (available_metadata
[key
] == None or available_metadata
[key
] == date(1,1,1)):
6048 self
._displayMessage
(
6049 u
"At least (%s) needs updating\n" % (key
))
6051 if key
== 'hash' and (available_metadata
['hash'] == u
'' or available_metadata
['hash'] == None):
6052 if (os
.path
.getsize(u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'])) < 65536 * 2):
6054 self
._displayMessage
(
6055 u
"At least (%s) needs updating\n" % (key
))
6057 if available_metadata
[key
] == None or available_metadata
[key
] == '' or available_metadata
[key
] == 'None' or available_metadata
[key
] == 'Unknown':
6058 self
._displayMessage
(
6059 u
"At least (%s) needs updating\n" % (key
))
6062 metadata_update
= False
6063 if not movie
and not len(available_metadata
['inetref']) >= 5:
6064 self
._displayMessage
(
6065 u
"At least (%s) needs updating\n" % ('inetref'))
6066 metadata_update
= True
6067 # Find the video file's real duration in minutes
6069 length
= self
._getVideoLength
(u
'%s/%s.%s' % (cfile
['filepath'], cfile
['filename'], cfile
['ext'], ))
6073 if length
!= available_metadata
['length']:
6074 self
._displayMessage
(u
"Video file real length (%d) minutes needs updating\n" % (length
))
6075 metadata_update
= True
6078 genres_cast
={'genres': u'', 'cast': u''}
6080 copy
= dict(available_metadata
)
6082 tmp_dict
= self
._getTmdbMetadata
(cfile
, copy
)
6084 tmp_dict
= self
._getTvdbMetadata
(cfile
, copy
)
6085 num_episode_metadata_downloads
+=1
6088 for key
in ['genres', 'cast', 'countries']:
6089 if tmp_dict
.has_key(key
):
6090 genres_cast
[key
] = tmp_dict
[key
]
6091 for key
in available_metadata
.keys():
6092 if key
in self
.config
['metadata_exclude_as_update_trigger']:
6095 if not tmp_dict
.has_key(key
):
6097 if key
== 'userrating' and available_metadata
[key
] == 0.0:
6098 available_metadata
[key
] = tmp_dict
[key
]
6102 length
= self
._getVideoLength
(u
'%s/%s.%s' %(cfile
['filepath'], cfile
['filename'], cfile
['ext'], ))
6106 available_metadata
['length'] = length
6108 available_metadata
[key
] = tmp_dict
[key
]
6110 available_metadata
[key
] = tmp_dict
[key
]
6112 # Fix fields that must be prepared for insertion into data base
6113 available_metadata
['title'] = self
._make
_db
_ready
(available_metadata
['title'])
6114 available_metadata
['director'] = self
._make
_db
_ready
(available_metadata
['director'])
6115 available_metadata
['plot'] = self
._make
_db
_ready
(available_metadata
['plot'])
6116 if available_metadata
['year'] == 0:
6117 available_metadata
['year'] = 1895
6118 if available_metadata
['coverfile'] == None:
6119 available_metadata
['coverfile'] = u
'No Cover'
6120 if len(genres_cast
['genres']) and available_metadata
['category'] == 'none':
6122 genres
= genres_cast
['genres'][:genres_cast
['genres'].index(',')]
6124 genres
= genres_cast
['genres']
6125 available_metadata
['category'] = genres
6126 self
._displayMessage
(u
"Category added for (%s)(%s)" % (available_metadata
['title'], available_metadata
['category']))
6128 # Make sure graphics relative/absolute paths are set PROPERLY based
6129 # on the 'filename' field being a relative or absolute path. A filename with an absolite path
6130 # CAN ONLY have graphics baed on absolute paths.
6131 # A filename with a relative path can have mixed absolute and relative path graphic files
6132 if available_metadata
[u
'filename'][0] == u
'/':
6133 available_metadata
[u
'host'] = u
''
6134 for key
in [u
'coverfile', u
'banner', u
'fanart']:
6135 if available_metadata
[key
] != None and available_metadata
[key
] != u
'No Cover' and available_metadata
[key
] != u
'':
6136 if available_metadata
[key
][0] != u
'/':
6137 tmp
= self
.rtnAbsolutePath(available_metadata
[key
], graphicsDirectories
[key
])
6139 if key
== u
'coverfile':
6140 available_metadata
[key
] = u
'No Cover'
6142 available_metadata
[key
] = u
''
6144 available_metadata
[u
'host'] = localhostname
.lower()
6146 ###############################################################################
6147 # END of metadata text logic - Checking, downloading, renaming
6148 ###############################################################################
6150 ###############################################################################
6151 # START of metadata updating the MythVideo record when graphics or text has changed
6152 ###############################################################################
6153 # Check if any new information was found
6154 if not self
.config
['overwrite']:
6155 for key
in available_metadata
.keys():
6156 if available_metadata
[key
] != meta_dict
[key
]:
6157 if available_metadata
[key
] == u
'' and meta_dict
[key
] == None:
6159 if available_metadata
[key
] == u
'' and meta_dict
[key
] == u
'Unknown':
6162 self
._displayMessage
(
6163 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
])))
6165 self
._displayMessage
(
6166 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
])))
6169 self
._displayMessage
(
6170 u
"Nothing to update for video file(%s)\n" % cfile
['filename']
6174 if self
.config
['simulation']:
6176 u
"Simulation MythTV DB update for video file(%s)\n" % cfile
['filename']
6178 for key
in available_metadata
.keys():
6179 print key
," ", available_metadata
[key
]
6180 for key
in genres_cast
.keys():
6181 sys
.stdout
.write(u
"Key(%s):(%s)\n" % (key
, genres_cast
[key
]))
6183 sys
.stdout
.write('\n')
6185 # Clean up a few fields before updating Mythdb
6186 if available_metadata
['showlevel'] == 0: # Allows mythvideo to display this video
6187 available_metadata
['showlevel'] = 1
6188 Video(id=intid
, db
=mythvideo
).update(available_metadata
)
6189 num_mythdb_updates
+=1
6190 videos_updated_metadata
.append(cfile
['filename'])
6191 for key
in ['genres', 'cast', 'countries']:
6192 if key
== 'genres' and len(cfile
['categories']):
6193 genres_cast
[key
]+=cfile
['categories']
6194 if genres_cast
.has_key(key
):
6195 self
._addCastGenreCountry
( genres_cast
[key
], Video(id=intid
, db
=mythvideo
), key
)
6196 self
._displayMessage
(
6197 u
"Updated Mythdb for video file(%s)\n" % cfile
['filename']
6199 ###############################################################################
6200 # END of metadata updating the MythVideo record when graphics or text has changed
6201 ###############################################################################
6203 sys
.stdout
.write(u
"\nMythtv video database maintenance ends at : %s\n" % (datetime
.datetime
.now()).strftime("%Y-%m-%d %H:%M"))
6206 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
))
6208 if len(videos_updated_metadata
):
6209 sys
.stdout
.write(u
'\n--------------Updated Video Files----------\n' )
6210 for videofile
in videos_updated_metadata
:
6211 sys
.stdout
.write(u
'%s\n' % videofile
)
6212 if len(missing_inetref
):
6213 sys
.stdout
.write(u
'\n----------------No Inetref Found-----------\n' )
6214 for videofile
in missing_inetref
:
6215 sys
.stdout
.write(u
'%s\n' % videofile
)
6216 if len(videos_with_small_posters
):
6217 sys
.stdout
.write(u
'\n---------------Under sized Poster----------\n' )
6218 for videofile
in videos_with_small_posters
:
6219 sys
.stdout
.write(u
'%s\n' % videofile
)
6220 if len(videos_using_imdb_numbers
):
6221 sys
.stdout
.write(u
'\n---------------Movies with IMDB#s----------\n' )
6222 for videofile
in videos_using_imdb_numbers
:
6223 sys
.stdout
.write(u
'%s\n' % videofile
)
6225 # end processMythTvMetaData
6227 def __repr__(self
): # Just a place holder
6231 # end MythTvMetaData
6233 def simple_example():
6234 """Simple example of using jamu
6235 Displays the poster graphics URL(s) and episode meta data for the TV series Sanctuary, season 1
6237 returns None if there was no data found for the request TV series
6238 returns False if there is no TV series as specified
6239 returns a string with poster URLs and episode meta data
6241 # Get an instance of the variable configuration information set to default values
6242 configuration
= Configuration(interactive
= True, debug
= False)
6244 # Set the type of data to be returned
6245 configuration
.changeVariable('get_poster', True)
6246 configuration
.changeVariable('get_ep_meta', True)
6248 # Validate specific variables and set the TV series information
6249 configuration
.validate_setVariables(['Sanctuary', '1', '3'])
6251 # Get an instance of the tvdb process function and fetch the data
6252 process
= Tvdatabase(configuration
.config
)
6253 results
= process
.processTVdatabaseRequests()
6255 if results
!= None and results
!= False: # Print the returned data string to the stdout
6256 print process
.processTVdatabaseRequests().encode('utf8')
6257 # end simple_example
6261 """Support jamu from the command line
6264 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'>")
6266 parser
.add_option( "-b", "--debug", action
="store_true", default
=False, dest
="debug",
6267 help=u
"Show debugging info")
6268 parser
.add_option( "-u", "--usage", action
="store_true", default
=False, dest
="usage",
6269 help=u
"Display the six main uses for this jamu")
6270 parser
.add_option( "-e", "--examples", action
="store_true", default
=False, dest
="examples",
6271 help=u
"Display examples for executing the jamu script")
6272 parser
.add_option( "-v", "--version", action
="store_true", default
=False, dest
="version",
6273 help=u
"Display version and author information")
6274 parser
.add_option( "-i", "--interactive", action
="store_true", default
=False, dest
="interactive",
6275 help=u
"Interactive mode allows selection of a specific Series from a series list")
6276 parser
.add_option( "-f", "--flags_options", action
="store_true", default
=False,dest
="flags_options",
6277 help=u
"Display all variables and settings then exit")
6278 parser
.add_option( "-l", "--language", metavar
="LANGUAGE", default
=u
'en', dest
="language",
6279 help=u
"Select data that matches the specified language fall back to english if nothing found (e.g. 'es' Español, 'de' Deutsch ... etc)")
6280 parser
.add_option( "-s", "--simulation", action
="store_true", default
=False, dest
="simulation",
6281 help=u
"Simulation (dry run), no downloads are performed or data bases altered")
6282 parser
.add_option( "-t", "--toprated", action
="store_true", default
=False, dest
="toprated",
6283 help=u
"Only display/download the top rated TV Series graphics")
6284 parser
.add_option( "-d", "--download", action
="store_true", default
=False, dest
="download",
6285 help=u
"Download and save the graphics and/or meta data")
6286 parser
.add_option( "-n", "--nokeys", action
="store_true", default
=False, dest
="nokeys",
6287 help=u
"Do not add data type keys to data values when displaying data")
6288 parser
.add_option( "-m", "--maximum", metavar
="MAX", default
=None, dest
="maximum",
6289 help=u
"Limit the number of graphics per type downloaded. e.g. --maximum=6")
6290 parser
.add_option( "-o", "--overwrite", action
="store_true", default
=False, dest
="overwrite",
6291 help=u
"Overwrite any matching files already downloaded")
6292 parser
.add_option( "-C", "--user_config", metavar
="FILE", default
="", dest
="user_config",
6293 help=u
"User specified configuration variables. e.g --user_config='~/.jamu/jamu.conf'")
6294 parser
.add_option( "-F", "--filename", action
="store_true", default
=False, dest
="ret_filename",
6295 help=u
"Display a formated filename for an episode")
6296 parser
.add_option( "-U", "--update", action
="store_true", default
=False, dest
="update",
6297 help=u
"Update a meta data file if local episode meta data is older than what is available on thetvdb.com")
6298 parser
.add_option( "-D", "--mythtvdir", action
="store_true", default
=False, dest
="mythtvdir",
6299 help=u
"Store graphic files into the MythTV DB specified dirs")
6300 parser
.add_option( "-M", "--mythtvmeta", action
="store_true", default
=False, dest
="mythtvmeta",
6301 help=u
"Add/update TV series episode or movie meta data in MythTV DB")
6302 parser
.add_option( "-V", "--mythtv_verbose", action
="store_true", default
=False, dest
="mythtv_verbose",
6303 help=u
"Display verbose messages when performing MythTV metadata maintenance")
6304 parser
.add_option( "-J", "--mythtvjanitor", action
="store_true", default
=False, dest
="mythtvjanitor",
6305 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.")
6306 parser
.add_option( "-N", "--mythtvNFS", action
="store_true", default
=False, dest
="mythtvNFS",
6307 help=u
"This option overrides Jamu's restrictions on processing NFS mounted Video and/or graphic files.")
6308 parser
.add_option( "-I", "--mythtv_inetref", action
="store_true", default
=False, dest
="mythtv_inetref",
6309 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.")
6310 parser
.add_option( "-W", "--mythtv_watched", action
="store_true", default
=False, dest
="mythtv_watched",
6311 help=u
"Download graphics for Scheduled and Recorded videos. This option is ONLY active if the -M option is also selected.")
6312 parser
.add_option( "-G", "--mythtv_guess", action
="store_true", default
=False, dest
="mythtv_guess",
6313 help=u
"Guess at the inetref for a video. This option is ONLY active if the -M option is also selected.")
6314 parser
.add_option( "-S", "--selected_data", metavar
="TYPES", default
=None, dest
="selected_data",
6315 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")
6316 parser
.add_option( "-R", "--mythtv_ref_num", action
="store_true", default
=False, dest
="mythtv_ref_num",
6317 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.")
6319 opts
, series_season_ep
= parser
.parse_args()
6323 print "\nargs", series_season_ep
6325 # Set the default configuration values
6326 if opts
.mythtv_inetref
or opts
.mythtv_ref_num
:
6327 opts
.interactive
= True
6328 configuration
= Configuration(interactive
= opts
.interactive
, debug
= opts
.debug
)
6330 if opts
.usage
: # Display usage information
6331 sys
.stdout
.write(usage_txt
+'\n')
6334 if opts
.examples
: # Display example information
6335 sys
.stdout
.write(examples_txt
+'\n')
6338 if opts
.version
== True: # Display program information
6339 sys
.stdout
.write(u
"\nTitle: (%s); Version: (%s); Author: (%s)\n%s\n" % (
6340 __title__
, __version__
, __author__
, __purpose__
))
6343 # Verify that only one instance of the following options is running at any one time
6344 # Options (-M, -MW and -MG)
6349 MythLog
._setlevel
('none') # There cannot be any logging messages with non -M options
6350 if opts
.mythtvmeta
and opts
.mythtv_watched
:
6352 if opts
.mythtvmeta
and opts
.mythtv_guess
:
6354 if opts
.mythtvmeta
and opts
.mythtvjanitor
: # No instance check with the janitor option
6356 if opts
.mythtvmeta
and opts
.mythtv_inetref
: # No instance check with the interactive mode option
6358 if options
in [u
'M', u
'MW', u
'MG']:
6359 jamu_instance
= singleinstance(u
'/tmp/Jamu_%s_instance.pid' % options
)
6361 # check is another instance of Jamu is running
6363 if jamu_instance
.alreadyrunning():
6364 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
6367 # Message the user that they are using incompatible options with the -MW option
6368 if opts
.mythtvmeta
and opts
.mythtv_watched
and (opts
.mythtv_inetref
or opts
.interactive
):
6369 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'
6372 # Message the user that they are using incompatible options -R and -I or -i
6373 if opts
.mythtvmeta
and opts
.mythtv_ref_num
and opts
.mythtv_inetref
:
6374 print u
'\n! Error: The (-R) and (-I) options are mutually exclusive.\nPlease change your options and try again.\n'
6377 # Apply any command line switches
6378 configuration
.changeVariable('local_language', opts
.language
)
6379 configuration
.changeVariable('simulation', opts
.simulation
)
6380 configuration
.changeVariable('toprated', opts
.toprated
)
6381 configuration
.changeVariable('download', opts
.download
)
6382 configuration
.changeVariable('nokeys', opts
.nokeys
)
6383 configuration
.changeVariable('maximum', opts
.maximum
)
6384 configuration
.changeVariable('overwrite', opts
.overwrite
)
6385 configuration
.changeVariable('ret_filename', opts
.ret_filename
)
6386 configuration
.changeVariable('update', opts
.update
)
6387 configuration
.changeVariable('mythtvdir', opts
.mythtvdir
)
6388 configuration
.changeVariable('mythtvmeta', opts
.mythtvmeta
)
6389 configuration
.changeVariable('mythtv_inetref', opts
.mythtv_inetref
)
6390 configuration
.changeVariable('mythtv_ref_num', opts
.mythtv_ref_num
)
6391 configuration
.changeVariable('mythtv_watched', opts
.mythtv_watched
)
6392 configuration
.changeVariable('mythtv_guess', opts
.mythtv_guess
)
6393 configuration
.changeVariable('mythtv_verbose', opts
.mythtv_verbose
)
6394 configuration
.changeVariable('mythtvjanitor', opts
.mythtvjanitor
)
6395 configuration
.changeVariable('mythtvNFS', opts
.mythtvNFS
)
6396 configuration
.changeVariable('data_flags', opts
.selected_data
)
6398 # Check if the user wants to change options via a configuration file
6399 if opts
.user_config
!= '': # Did the user want to override the default config file name/location
6400 configuration
.setUseroptions(opts
.user_config
)
6402 default_config
= u
"%s/%s" % (os
.path
.expanduser(u
"~"), u
".mythtv/jamu.conf")
6403 if os
.path
.isfile(default_config
):
6404 configuration
.setUseroptions(default_config
)
6406 print u
"\nThere was no default Jamu configuration file found (%s)\n" % default_config
6408 if opts
.flags_options
: # Display option variables
6409 if len(series_season_ep
):
6410 configuration
.validate_setVariables(series_season_ep
)
6412 configuration
.validate_setVariables(['FAKE SERIES NAME','FAKE EPISODE NAME'])
6413 configuration
.displayOptions()
6416 # Validate specific variables
6417 configuration
.validate_setVariables(series_season_ep
)
6419 if configuration
.config
['mythtvmeta']:
6420 process
= MythTvMetaData(configuration
.config
)
6421 process
.processMythTvMetaData()
6422 elif configuration
.config
['video_dir']:
6423 process
= VideoFiles(configuration
.config
)
6424 results
= process
.processFileOrDirectory()
6425 if results
!= None and results
!= False:
6426 print process
.processFileOrDirectory().encode('utf8')
6428 process
= Tvdatabase(configuration
.config
)
6429 results
= process
.processTVdatabaseRequests()
6430 if results
!= None and results
!= False:
6431 print process
.processTVdatabaseRequests().encode('utf8')
6435 if __name__
== "__main__":