]> git.rmz.io Git - dotfiles.git/blob - bin/old/jamu.py
cleaning up bin
[dotfiles.git] / bin / old / jamu.py
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3 # ----------------------
4 # Name: jamu.py Just.Another.Metadata.Utility
5 # Python Script
6 # Author: R.D. Vaughan
7 # Purpose: This python script is intended to perform a variety of utility functions on mythvideo
8 # metadata and the associated video files.
9 #
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
14 # the script.
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
23 # thetvdb.com
24 # Python access to the tmdb api started with a module from dbr/Ben and then enhanced for
25 # Jamu's needs.
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>.
31 #
32 # Command line examples:
33 # See help (-u and -h) options
34 #
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"
40 __purpose__='''
41 This python script is intended to perform a variety of utility functions on mythvideo metadata
42 and the associated video files.
43
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.
48 '''
49
50 __version__=u"v0.7.8"
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
88 # available.
89 # 0.3.6 Fixed bug when searching themoviedb.com for a movie by title or
90 # alternate title.
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
104 # paths.
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
117 # processing.
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
126 # preserved.
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
143 # read and writable.
144 # Fixed a bug where under rare circumstances a graphic would be repeatedly
145 # downloaded.
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
149 # numbers.
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
187 # change set [21191]
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
197 # site issues.
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
213 # proper file path.
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
263 # extended period.
264 # Added a new jamu.conf section [ignore-directory] to list Video sub-directories that Jamu should
265 # ignore.
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
273 # re-downloaded.
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
286 # for a movie.
287 # Jamu will now download the top rated TV Series season coverart and banner images. This enhancement
288 # matches MythVideo processing.
289 # 0.6.5 Small fix related to the bindings changes.
290 # 0.6.6 Fixed Exception messages
291 # Change all occurances of 'mythbeconn.host' to 'mythbeconn.hostname' to be consistent with bindings
292 # 0.6.7 Fixed the (-J) janitor option from removing the Mirobridge default images when they are not being used
293 # 0.6.8 Fixed a (-J) janitor option statistics error due to skipping Mirbridge default images
294 # 0.6.9 Fixed an abort when IMDBpy returns movie matches with incomplete data
295 # Fixed an abort where an IMDB# was being used instead of a TMDB#
296 # Fixed an abort when a storage directory name caused an UnicodeEncodeError or TypeError exception
297 # 0.7.0 Fixed an (-MW) option abort when a recorded program or upcoming program did not have a title
298 # 0.7.1 Fixed a bug where movies with punctutation ("Mr. Magoo") were not finding matches
299 # Fixed bug with interactive mode when a user enters a reference number directly rather than
300 # making a list selection
301 # These bugs were both identified by Edi Iten (thanks)
302 # 0.7.2 Fixed a bug where an inetref field was not properly initialized and caused an abort. Ticket #8243
303 # 0.7.3 Fixed a bug where a user selected TMDB# was not being used.
304 # Minor change to fuzzy matching of a file named parsed title with those from TMDB and TVDB.
305 # 0.7.4 Update for changes in Python bindings
306 # 0.7.5 Added the TMDB MovieRating as videometadata table "rating" field
307 # 0.7.6 Modifications to support MythTV python bindings changes
308 # 0.7.7 Pull hostname from python bindings instead of socket libraries
309 # Added support of unicode characters within a jamu.conf file
310 # Replace 'xml' module version check with generic Python version, to correct failure in Python 2.7
311 # 0.7.8 Fixed a bug which caused jamu to crash due to an extra unicode conversion introduced in 0.7.7.
312 # See also #9637.
313
314
315 usage_txt=u'''
316 JAMU - Just.Another.Metadata.Utility is a versatile utility for downloading graphics and meta data
317 for both movies and TV Series information from themoviedb.com wiki and thetvdb.com wiki. In addition
318 the MythTV data base is updated with the downloaded information.
319 Here are the main uses for this utility:
320 MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
321
322 1) Simple command line invocation to display or download data from thetvdb.com.
323 Data can be one or more of: Posters/Cover art, Banners, Fan art,
324 Episode Images and Episode meta data. use the command "jamu -e | less" to see
325 command line examples.
326 2) Mass downloads of data matching your video files. **
327 This typically done once to download the information for your video collection.
328 3) Automated maintenance of the information in your video collection. **
329 4) The creation of video file names which can be used to set the file name of your recorded TV shows.
330 File names can be formated to the users preference with information like series name, season number,
331 episode number and episode name. MythTV users may find this valuable as part of a user job
332 that is spawned automatically by mythbackend when recording is finished.
333 5) Jamu's modules can be imported into your own python scripts to create enhanced functionality.
334 6) With the installation of free ImageMagick's utilities (specifically 'mogrify') you can resize
335 graphics when they are downloaded.
336 7) Update the MythTV data base with links to posters, banners, fanart and episode images and optionally
337 download missing graphics if they exist. This feature can be used for mass updates and regular
338 maintenance.
339
340 '''
341
342 examples_txt=u'''
343 MythTV users should review the Jamu wiki page at http://www.mythtv.org/wiki/Jamu for details.
344 These examples are primarily for non-MythTV users of Jamu.
345
346 jamu command line examples:
347 NOTE: Included here are simple examples of jamu in action.
348 Please review jamu_README for advise on how to get the most out of jamu.
349
350 ( Display a TV series top rated poster fanart and banner URLs)
351 > jamu -tS PBF "Sanctuary"
352 poster:http://www.thetvdb.com/banners/posters/80159-1.jpg
353 fanart:http://www.thetvdb.com/banners/fanart/original/80159-2.jpg
354 banner:http://www.thetvdb.com/banners/graphical/80159-g2.jpg
355
356 ( Display the URL for a TV series episode )
357 > jamu -tS I "Fringe" 1 5
358 filename:http://www.thetvdb.com/banners/episodes/82066-391049.jpg
359
360 ( Display poster, fanart and banner graphics for a TV series but limited to two per type in a season )
361 > jamu -S PBF -m 2 "24" 4
362 poster:http://www.thetvdb.com/banners/seasons/76290-4-3.jpg
363 poster:http://www.thetvdb.com/banners/seasons/76290-4.jpg
364 fanart:http://www.thetvdb.com/banners/fanart/original/76290-1.jpg
365 fanart:http://www.thetvdb.com/banners/fanart/original/76290-2.jpg
366 banner:http://www.thetvdb.com/banners/seasonswide/76290-4.jpg
367 banner:http://www.thetvdb.com/banners/seasonswide/76290-4-3.jpg
368
369 ( Display a file name string (less file extention and directory path) for a TV episode )
370 > jamu -F "24" 4 3
371 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
372
373 > jamu -F "24" "Day 4: 9:00 A.M.-10:00 A.M."
374 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
375
376 ( Using SID number instead of series name )
377 > jamu -F 76290 4 3
378 24 - S04E03 - Day 4: 9:00 A.M.-10:00 A.M.
379
380 ( Simulate a dry run for the download of a TV series top rated poster and fanart )
381 > jamu -sdtS PF "Fringe"
382 Simulation download of URL(http://www.thetvdb.com/banners/posters/82066-6.jpg) to File(~/Pictures/Poster - 82066-6.jpg)
383 Get_Poster downloading successfully processed
384 Simulation download of URL(http://www.thetvdb.com/banners/fanart/original/82066-11.jpg) to File(~/Pictures/Fanart - 82066-11.jpg)
385 Get_Fanart downloading successfully processed
386
387 ( Download the Episode meta data and episode image for a video file whose file name contains the series and season/episode numbers)
388 > jamu -dS EI "~/Pictures/Fringe - S01E01.mkv"
389 Episode meta data and/or images downloads successfully processed
390 > ls -ls
391 total 2
392 60 -rw-r--r-- 1 user user 53567 2009-03-12 22:05 Fringe - S01E01 - Pilot.jpg
393 4 -rw-r--r-- 1 user user 1059 2009-03-12 22:05 Fringe - S01E01 - Pilot.meta
394 4 -rw-r--r-- 1 user user 811 2009-03-12 13:22 Fringe - S01E01.mkv
395
396 ( Display Episode meta data for a TV series )
397 > jamu -S E "24" 5 3
398 series:24
399 seasonnumber:5
400 episodenumber:3
401 episodename:Day 5: 9:00 A.M.-10:00 A.M.
402 rating:None
403 overview:Jack conceals himself inside the airport hanger and surveys the Russian separatists, feeding information to Curtis and his assault team.
404 The terrorists begin executing hostages in an attempt to make Logan cave into their demands.
405 Martha discovers that all traces of her conversation with Palmer may not have been erased.
406 director:Brad Turner
407 writer:Manny Coto
408 gueststars:John Gleeson Connolly, V.J. Foster, David Dayan Fisher, Taylor Nichols, Steve Edwards, Taras Los, Joey Munguia, Reggie Jordan, Lou Richards, Karla Zamudio
409 imdb_id:None
410 filename:http://www.thetvdb.com/banners/episodes/76290-306117.jpg
411 epimgflag:None
412 language:en
413 firstaired:2006-01-16
414 lastupdated:1197942225
415 productioncode:5AFF03
416 id:306117
417 seriesid:76290
418 seasonid:10067
419 absolute_number:None
420 combined_season:5
421 combined_episodenumber:4.0
422 dvd_season:5
423 dvd_discid:None
424 dvd_chapter:None
425 dvd_episodenumber:4.0
426
427 ( Specify a user defined configuration file to set most of the configuration variables )
428 > jamu -C "~/.jamu/jamu.conf" -S P "Supernatural"
429 poster:http://www.thetvdb.com/banners/posters/78901-3.jpg
430 poster:http://www.thetvdb.com/banners/posters/78901-1.jpg
431
432 ( Display in alphabetical order the state of all configuration variables )
433 > jamu -f
434 allgraphicsdir (~/Pictures)
435 bannerdir (None)
436 config_file (False)
437 data_flags (None)
438 debug_enabled (False)
439 download (False)
440 ... lots of configuration variables ...
441 video_dir (None)
442 video_file_exts (['3gp', 'asf', 'asx', 'avi', 'mkv', 'mov', 'mp4', 'mpg', 'qt', 'rm', 'swf', 'wmv', 'm2ts', 'evo', 'ts', 'img', 'iso'])
443 with_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s)
444 without_ep_name (%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s)
445 '''
446
447 # System modules
448 import sys, os, re, locale, subprocess, locale, ConfigParser, urllib, codecs, shutil, datetime, fnmatch, string
449 from datetime import date
450 from optparse import OptionParser
451 from socket import gethostbyname
452 import tempfile, struct
453 import logging
454
455 class OutStreamEncoder(object):
456 """Wraps a stream with an encoder"""
457 def __init__(self, outstream, encoding=None):
458 self.out = outstream
459 if not encoding:
460 self.encoding = sys.getfilesystemencoding()
461 else:
462 self.encoding = encoding
463
464 def write(self, obj):
465 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
466 if isinstance(obj, unicode):
467 try:
468 self.out.write(obj.encode(self.encoding))
469 except IOError:
470 pass
471 else:
472 try:
473 self.out.write(obj)
474 except IOError:
475 pass
476
477 def __getattr__(self, attr):
478 """Delegate everything but write to the stream"""
479 return getattr(self.out, attr)
480 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
481 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
482
483 if sys.version_info <= (2,5):
484 print '''JAMU requires Python 2.5 or newer to run.'''
485 sys.exit(1)
486
487 import xml.etree.cElementTree as ElementTree
488
489
490 # Find out if the MythTV python bindings can be accessed and instances can be created
491 try:
492 '''If the MythTV python interface is found, we can insert data directly to MythDB or
493 get the directories to store poster, fanart, banner and episode graphics.
494 '''
495 from MythTV import MythDB, Video, MythVideo, MythBE, MythError, MythLog, RecordedProgram
496 from MythTV.database import DBData
497 mythdb = None
498 mythvideo = None
499 mythbeconn = None
500 try:
501 '''Create an instance of each: MythDB, MythVideo
502 '''
503 MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout
504 mythdb = MythDB()
505 mythvideo = MythVideo(mythdb)
506 MythLog._setlevel('important,general')
507 except MythError, e:
508 print u'\n! Warning - %s' % e.args[0]
509 filename = os.path.expanduser("~")+'/.mythtv/config.xml'
510 if not os.path.isfile(filename):
511 print u'\n! Warning - A correctly configured (%s) file must exist\n' % filename
512 else:
513 print u'\n! Warning - Check that (%s) is correctly configured\n' % filename
514 except Exception, e:
515 print u"\n! Warning - Creating an instance caused an error for one of: MythDB or MythVideo, error(%s)\n" % e
516 localhostname = mythdb.gethostname()
517 try:
518 MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout
519 mythbeconn = MythBE(backend=localhostname, db=mythdb)
520 MythLog._setlevel('important,general')
521 except MythError, e:
522 print u'\nWith any -M option Jamu must be run on a MythTV backend'
523 print u'! Warning - %s' % e.args[0]
524 mythbeconn = None
525 except Exception, e:
526 print u"\n! Warning - MythTV python bindings could not be imported, error(%s)\n" % e
527 mythdb = None
528 mythvideo = None
529 mythbeconn = None
530
531
532 # Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available
533 try:
534 # thetvdb.com specific modules
535 import MythTV.ttvdb.tvdb_ui as tvdb_ui
536 # from tvdb_api import Tvdb
537 import MythTV.ttvdb.tvdb_api as tvdb_api
538 from MythTV.ttvdb.tvdb_exceptions import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_episodenotfound, tvdb_attributenotfound, tvdb_userabort)
539
540 # verify version of tvdbapi to make sure it is at least 1.0
541 if tvdb_api.__version__ < '1.0':
542 print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api.__version__
543 raise
544 except Exception, e:
545 print '''
546 The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py.
547 They should have been installed along with the MythTV python bindings.
548 Error(%s)
549 ''' % e
550 sys.exit(1)
551
552
553 try:
554 import MythTV.tmdb.tmdb_api as tmdb_api
555 from MythTV.tmdb.tmdb_exceptions import (TmdBaseError, TmdHttpError, TmdXmlError, TmdbUiAbort, TmdbMovieOrPersonNotFound,)
556 except Exception, e:
557 sys.stderr.write('''
558 The subdirectory "tmdb" containing the modules tmdb_api.py (v0.1.3 or greater), tmdb_ui.py,
559 tmdb_exceptions.py must have been installed with the MythTV python bindings.
560 Error:(%s)
561 ''' % e)
562 sys.exit(1)
563
564 if tmdb_api.__version__ < '0.1.3':
565 sys.stderr.write("\n! Error: Your current installed tmdb_api.py version is (%s)\nYou must at least have version (0.1.3) or higher.\n" % tmdb_api.__version__)
566 sys.exit(1)
567
568
569 imdb_lib = True
570 try: # Check if the installation is equiped to directly search IMDB for movies
571 import imdb
572 except ImportError, e:
573 sys.stderr.write("\n! Error: To search for movies movies the IMDbPy library must be installed."\
574 "Check your installation's repository or check the following link."\
575 "from (http://imdbpy.sourceforge.net/?page=download)\nError:(%s)\n" % e)
576 sys.exit(1)
577
578 if imdb_lib:
579 if imdb.__version__ < "3.8":
580 sys.stderr.write("\n! Error: You version the IMDbPy library (%s) is too old. You must use version 3.8 of higher." % imdb.__version__)
581 sys.stderr.write("Check your installation's repository or check the following link."\
582 "from (http://imdbpy.sourceforge.net/?page=download)\n")
583 sys.exit(1)
584
585 class VideoTypes( DBData ):
586 _table = 'videotypes'
587 _where = 'intid=%s'
588 _setwheredat = 'self.intid,'
589 _logmodule = 'Python VideoType'
590 def __str__(self):
591 return "<VideoTypes '%s'>" % self.extension
592 def __repr__(self):
593 return str(self).encode('utf-8')
594 def __init__(self, id=None, ext=None, db=None):
595 if id is not None:
596 DBData.__init__(self, data=(id,), db=db)
597 elif ext is not None:
598 self.__dict__['_where'] = 'extension=%s'
599 self.__dict__['_wheredat'] = 'self.extension,'
600 DBData.__init__(self, data=(ext,), db=db)
601 else:
602 DBData.__init__(self, None, db=db)
603 # end VideoTypes()
604
605 def isValidPosixFilename(name, NAME_MAX=255):
606 """Checks for a valid POSIX filename
607
608 Filename: a name consisting of 1 to {NAME_MAX} bytes used to name a file.
609 The characters composing the name may be selected from the set of
610 all character values excluding the slash character and the null byte.
611 The filenames dot and dot-dot have special meaning.
612 A filename is sometimes referred to as a "pathname component".
613
614 name: (base)name of the file
615 NAME_MAX: is defined in limits.h (implementation-defined constants)
616 Maximum number of bytes in a filename
617 (not including terminating null).
618 Minimum Acceptable Value: {_POSIX_NAME_MAX}
619 _POSIX_NAME_MAX: Maximum number of bytes in a filename
620 (not including terminating null).
621 Value: 14
622
623 More information on http://www.opengroup.org/onlinepubs/009695399/toc.htm
624 """
625 return 1<=len(name)<= NAME_MAX and "/" not in name and "\000" not in name
626 # end isValidPosixFilename()
627
628
629 # Two routines used for movie title search and matching
630 def is_punct_char(char):
631 '''check if char is punctuation char
632 return True if char is punctuation
633 return False if char is not punctuation
634 '''
635 return char in string.punctuation
636
637 def is_not_punct_char(char):
638 '''check if char is not punctuation char
639 return True if char is not punctuation
640 return False if chaar is punctuation
641 '''
642 return not is_punct_char(char)
643
644 def _getExtention(URL):
645 """Get the graphic file extension from a URL
646 return the file extention from the URL
647 """
648 (dirName, fileName) = os.path.split(URL)
649 (fileBaseName, fileExtension)=os.path.splitext(fileName)
650 return fileExtension[1:]
651 # end getExtention
652
653 def _getFileList(dst):
654 ''' Create an array of fully qualified file names
655 return an array of file names
656 '''
657 file_list = []
658 names = []
659
660 try:
661 for directory in dst:
662 try:
663 directory = unicode(directory, 'utf8')
664 except (UnicodeEncodeError, TypeError):
665 pass
666 for filename in os.listdir(directory):
667 names.append(os.path.join(directory, filename))
668 except OSError, e:
669 sys.stderr.write(u"\n! Error: Getting a list of files for directory (%s)\nThis is most likely a 'Permission denied' error\nError:(%s)\n\n" % (dst, e))
670 return file_list
671
672 for video_file in names:
673 if os.path.isdir(video_file):
674 new_files = _getFileList([video_file])
675 for new_file in new_files:
676 file_list.append(new_file)
677 else:
678 file_list.append(video_file)
679 return file_list
680 # end _getFileList
681
682
683 class singleinstance(object):
684 '''
685 singleinstance - based on Windows version by Dragan Jovelic this is a Linux
686 version that accomplishes the same task: make sure that
687 only a single instance of an application is running.
688 '''
689
690 def __init__(self, pidPath):
691 '''
692 pidPath - full path/filename where pid for running application is to be
693 stored. Often this is ./var/<pgmname>.pid
694 '''
695 from os import kill
696 self.pidPath=pidPath
697 #
698 # See if pidFile exists
699 #
700 if os.path.exists(pidPath):
701 #
702 # Make sure it is not a "stale" pidFile
703 #
704 try:
705 pid=int(open(pidPath, 'r').read().strip())
706 #
707 # Check list of running pids, if not running it is stale so
708 # overwrite
709 #
710 try:
711 kill(pid, 0)
712 pidRunning = 1
713 except OSError:
714 pidRunning = 0
715 if pidRunning:
716 self.lasterror=True
717 else:
718 self.lasterror=False
719 except:
720 self.lasterror=False
721 else:
722 self.lasterror=False
723
724 if not self.lasterror:
725 #
726 # Write my pid into pidFile to keep multiple copies of program from
727 # running.
728 #
729 fp=open(pidPath, 'w')
730 fp.write(str(os.getpid()))
731 fp.close()
732
733 def alreadyrunning(self):
734 return self.lasterror
735
736 def __del__(self):
737 if not self.lasterror:
738 import os
739 os.unlink(self.pidPath)
740 # end singleinstance()
741
742
743 # Global variables
744 graphicsDirectories = {'banner': u'bannerdir', 'screenshot': u'episodeimagedir', 'coverfile': u'posterdir', 'fanart': u'fanartdir'}
745 dir_dict={'posterdir': "VideoArtworkDir", 'bannerdir': 'mythvideo.bannerDir', 'fanartdir': 'mythvideo.fanartDir', 'episodeimagedir': 'mythvideo.screenshotDir', 'mythvideo': 'VideoStartupDir'}
746 storagegroupnames = {u'Videos': u'mythvideo', u'Coverart': u'posterdir', u'Banners': u'bannerdir', u'Fanart': u'fanartdir', u'Screenshots': u'episodeimagedir'}
747 storagegroups={u'mythvideo': [], u'posterdir': [], u'bannerdir': [], u'fanartdir': [], u'episodeimagedir': []} # The gobal dictionary is only populated with the current hosts storage group entries
748 image_extensions = ["png", "jpg", "bmp"]
749
750 def getStorageGroups():
751 '''Populate the storage group dictionary with the local host's storage groups.
752 return nothing
753 '''
754 records = mythdb.getStorageGroup(hostname=localhostname)
755 for record in records:
756 # Only include Video, coverfile, banner, fanart, screenshot and trailers storage groups
757 if record.groupname in storagegroupnames.keys():
758 dirname = record.dirname
759 try:
760 dirname = unicode(record.dirname, 'utf8')
761 except (UnicodeDecodeError):
762 sys.stderr.write(u"\n! Error: The local Storage group (%s) directory contained\ncharacters that caused a UnicodeDecodeError. This storage group has been rejected.'\n" % (record['groupname']))
763 continue # Skip any line that has non-utf8 characters in it
764 except (UnicodeEncodeError, TypeError):
765 pass
766 # Strip the trailing slash so it is consistent with all other directory paths in Jamu
767 if dirname[-1:] == u'/':
768 storagegroups[storagegroupnames[record.groupname]].append(dirname[:-1])
769 else:
770 storagegroups[storagegroupnames[record.groupname]].append(dirname)
771 continue
772
773 any_storage_group = False
774 tmp_storagegroups = dict(storagegroups)
775 for key in tmp_storagegroups.keys():
776 if len(tmp_storagegroups[key]):
777 any_storage_group = True
778 else:
779 del storagegroups[key] # Remove empty SG directory arrays
780 if any_storage_group:
781 # Verify that each storage group is an existing local directory
782 storagegroup_ok = True
783 for key in storagegroups.keys():
784 for directory in storagegroups[key]:
785 if not os.access(directory, os.F_OK):
786 sys.stderr.write(u"\n! Error: The local Storage group (%s) directory (%s) does not exist\n" % (key, directory))
787 storagegroup_ok = False
788 if not storagegroup_ok:
789 sys.exit(1)
790 # end getStorageGroups
791
792
793 def _can_int(x):
794 """Takes a string, checks if it is numeric.
795 >>> _can_int("2")
796 True
797 >>> _can_int("A test")
798 False
799 """
800 if x == None:
801 return False
802 try:
803 int(x)
804 except ValueError:
805 return False
806 else:
807 return True
808 # end _can_int
809
810 class BaseUI:
811 """Default non-interactive UI, which auto-selects first results
812 """
813 def __init__(self, config, log):
814 self.config = config
815 self.log = log
816
817 def selectSeries(self, allSeries):
818 return allSeries[0]
819
820 def selectMovieOrPerson(self, allElements):
821 return makeDict([allElements[0]])
822
823 # Local variable
824 video_type = u''
825 UI_title = u''
826 UI_search_language = u''
827 UI_selectedtitle = u''
828 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
829 # Hard-coded here as it is realtively static, and saves another HTTP request, as
830 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
831 UI_langid_dict = {u'da': u'10', 'fi': u'11', u'nl': u'13', u'de': u'14', u'it': u'15', u'es': u'16', u'fr': u'17', u'pl': u'18', u'hu': u'19', u'el': u'20', u'tr': u'21', u'ru': u'22', u'he': u'24', u'ja': u'25', u'pt': u'26', u'zh': u'27', u'cs': u'28', u'sl': u'30', u'hr': u'31', u'ko': u'32', u'en': '7', u'sv': u'8', u'no': u'9',}
832
833 class jamu_ConsoleUI(BaseUI):
834 """Interactively allows the user to select a show or movie from a console based UI
835 """
836
837 def _displaySeries(self, allSeries_array):
838 """Helper function, lists series with corresponding ID
839 """
840 if video_type == u'IMDB':
841 URL = u'http://www.imdb.com/title/tt'
842 URL2 = u'http://www.imdb.com/find?s=all&q='+urllib.quote_plus(UI_title.encode("utf-8"))+'&x=0&y=0'
843 reftype = u'IMDB'
844 elif video_type == u'TMDB':
845 URL = u'http://themoviedb.org/movie/'
846 URL2 = u'http://themoviedb.org/'
847 reftype = u'TMDB'
848 else: # TVDB
849 URL = u'http://thetvdb.com/index.php?tab=series&id=%s&lid=%s'
850 URL2 = u'http://thetvdb.com/?tab=advancedsearch'
851 reftype = u'thetvdb'
852 tmp_title = u''
853
854 allSeries={}
855 for index in range(len(allSeries_array)):
856 allSeries[allSeries_array[index]['name']] = allSeries_array[index]
857 tmp_names = allSeries.keys()
858 tmp_names.sort()
859 most_likely = []
860
861 # Find any TV Shows or Movies who's titles start with the video's title
862 for name in tmp_names:
863 if filter(is_not_punct_char, name.lower()).startswith(filter(is_not_punct_char, UI_title.lower())):
864 most_likely.append(name)
865
866 # IMDB can return titles that are a movies foriegn title. The titles that do not match
867 # the requested title need to be added to the end of the most likely titles list.
868 if video_type == u'IMDB' and len(most_likely):
869 for name in tmp_names:
870 try:
871 dummy = most_likely.index(name)
872 except ValueError:
873 most_likely.append(name)
874
875 names = []
876 # Remove any name that does not start with a title like the TV Show/Movie (except for IMDB)
877 if len(most_likely):
878 for likely in most_likely:
879 names.append(likely)
880 else:
881 names = tmp_names
882
883 if not video_type == u'IMDB':
884 names.sort()
885
886 # reorder the list of series and sid's
887 new_array=[]
888 for key in names: # list all search results
889 new_array.append(allSeries[key])
890
891 # If there is only one to select and it is an exact match then return with no interface display
892 if len(new_array) == 1:
893 if filter(is_not_punct_char, allSeries_array[0]['name'].lower()) == filter(is_not_punct_char, UI_title.lower()):
894 return new_array
895
896 # Add the ability to select the skip inetref of '99999999'
897 new_array.append( {'sid': '99999999', 'name': u'User choses to ignore video'} )
898 names.append(u'User choses to ignore video')
899
900 i_show=0
901 for key in names: # list all search results
902 i_show+=1 # Start at more human readable number 1 (not 0)
903 if key == u'User choses to ignore video':
904 print u"% 2s -> %s # %s" % (
905 i_show,
906 '99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
907 )
908 continue
909 if video_type != u'IMDB' and video_type != u'TMDB':
910 tmp_URL = URL % (allSeries[key]['sid'], UI_langid_dict[UI_search_language])
911 print u"% 2s -> %s # %s" % (
912 i_show,
913 key, tmp_URL
914 )
915 else:
916 print u"% 2s -> %s # %s%s" % (
917 i_show,
918 key, URL,
919 allSeries[key]['sid']
920 )
921 print u"Direct search of %s # %s" % (
922 reftype,
923 URL2
924 )
925 return new_array
926
927 def selectSeries(self, allSeries):
928 global UI_selectedtitle
929 UI_selectedtitle = u''
930 allSeries = self._displaySeries(allSeries)
931
932 # Check for an automatic choice
933 if len(allSeries) <= 2:
934 for series in allSeries:
935 if filter(is_not_punct_char, series['name'].lower()) == filter(is_not_punct_char, UI_title.lower()):
936 UI_selectedtitle = series['name']
937 return series
938
939 display_total = len(allSeries)
940
941 if video_type == u'IMDB':
942 reftype = u'IMDB #'
943 refsize = 7
944 refformat = u"%07d"
945 elif video_type == u'TMDB':
946 reftype = u'TMDB #'
947 refsize = 5
948 refformat = u"%05d"
949 else:
950 reftype = u'Series id'
951 refsize = 5
952 refformat = u"%6d" # Attempt to have the most likely TV/Movies at the top of the list
953
954 while True: # return breaks this loop
955 try:
956 print u'Enter choice ("Enter" key equals first selection (1)) or input the %s directly, ? for help):' % reftype
957 ans = raw_input()
958 except KeyboardInterrupt:
959 raise tvdb_userabort(u"User aborted (^c keyboard interupt)")
960 except EOFError:
961 raise tvdb_userabort(u"User aborted (EOF received)")
962
963 self.log.debug(u'Got choice of: %s' % (ans))
964 try:
965 if ans == '':
966 selected_id = 0
967 else:
968 selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
969 except ValueError: # Input was not number
970 if ans == u"q":
971 self.log.debug(u'Got quit command (q)')
972 raise tvdb_userabort(u"User aborted ('q' quit command)")
973 elif ans == u"?":
974 print u"## Help"
975 print u"# Enter the number that corresponds to the correct video."
976 print u"# Enter the %s number for the %s." % (reftype, video_type)
977 print u"# ? - this help"
978 print u"# q - abort"
979 else:
980 self.log.debug(u'Unknown keypress %s' % (ans))
981 else:
982 self.log.debug(u'Trying to return ID: %d' % (selected_id))
983 try:
984 UI_selectedtitle = allSeries[selected_id]['name']
985 return allSeries[selected_id]
986 except IndexError:
987 if len(ans) == refsize and reftype != u'Series id':
988 UI_selectedtitle = u''
989 return {'name': u'User input', 'sid': ans}
990 elif reftype == u'Series id':
991 if len(ans) >= refsize:
992 UI_selectedtitle = u''
993 return {'name': u'User input', 'sid': ans}
994 self.log.debug(u'Invalid number entered!')
995 print u'Invalid number (%d) input! A directly entered %s must be a full %d zero padded digits (e.g. 905 should be entered as %s)' % (selected_id, reftype, refsize, refformat % 905)
996 UI_selectedtitle = u''
997 self._displaySeries(allSeries)
998 #end try
999 #end while not valid_input
1000
1001 def _useImageMagick(cmd):
1002 """ Process graphics files using ImageMagick's utility 'mogrify'.
1003 >>> _useImageMagick('-resize 50% "poster.jpg"')
1004 >>> 0
1005 >>> -1
1006 """
1007 return subprocess.call(u'mogrify %s > /dev/null' % cmd, shell=True)
1008 # end verifyImageMagick
1009
1010 # Call a execute a command line process
1011 def callCommandLine(command):
1012 '''Call a command line script or program. Display any errors
1013 return all stdoutput as a string
1014 '''
1015 p = subprocess.Popen(command, shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
1016
1017 while 1:
1018 data = p.stderr.readline()
1019 if len(data):
1020 sys.stderr.write(u'%s\n' % data)
1021 if data == '' and p.poll() != None:
1022 break
1023
1024 returned_data=''
1025 while 1:
1026 data = p.stdout.readline()
1027 if len(data):
1028 returned_data+=data
1029 if data == '' and p.poll() != None:
1030 break
1031 return returned_data
1032 # end callCommandLine
1033
1034
1035 # All functionality associated with configuration options
1036 class Configuration(object):
1037 """Set defaults, apply user configuration options, validate configuration settings and display the
1038 settings.
1039 To view all available options run:
1040 >>> config = Configuration()
1041 >>> config.displayOptions()
1042 """
1043 def __init__(self, interactive = False, debug = False):
1044 """Initialize default configuration settings
1045 """
1046 self.config = {}
1047 # Set all default variables
1048 self.config['interactive'] = interactive
1049 self.config['debug_enabled'] = debug
1050 self.config['flags_options'] = False
1051 self.config['local_language'] = u'en'
1052 self.config['simulation'] = False
1053 self.config['toprated'] = False
1054 self.config['download'] = False
1055 self.config['nokeys'] = False
1056 self.config['maximum'] = None
1057 self.config['user_config'] = None
1058 self.config['overwrite'] = False
1059 self.config['update'] = False
1060 self.config['mythtvdir'] = False
1061 self.config['hd_dvd'] = ' HD - On DVD' # Used for HD DVD collection zero length video files
1062 self.config['dvd'] = ' - On DVD' # Used for DVD collection zero length video files
1063
1064 self.config['video_dir'] = None
1065 self.config['recursive'] = True
1066 self.config['series_name'] = None
1067 self.config['sid'] = None
1068 self.config['season_num'] = None
1069 self.config['episode_num'] = None
1070 self.config['episode_name'] = None
1071 self.config['ret_filename'] = False
1072
1073 # Flags for which data to perform actions on
1074 self.config['get_poster'] = False
1075 self.config['get_banner'] = False
1076 self.config['get_fanart'] = False
1077 self.config['get_ep_image'] = False
1078 self.config['get_ep_meta'] = False
1079 self.config['data_flags'] = ''
1080 self.config['tmdb_genre_filter'] = ['action film', 'adventure film', 'comedy', 'crime film', 'disaster film', 'documentary film', 'drama film', 'eastern', 'environmental', 'fantasy film', 'historical film', 'horror film', 'musical film', 'mystery', 'mystery film', 'road movie', 'science fiction film', 'sport', 'thriller', 'western', 'film noir', 'cult movie', 'neo-noir', 'guy movie',]
1081
1082 self.config['log'] = self._initLogger() # Setups the logger (self.log.debug() etc)
1083
1084 # The default format of the file names (with and without episode names)
1085 self.config['with_ep_name'] = u'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d - %(episodename)s.%(ext)s'
1086 self.config['without_ep_name'] = u'%(series)s - S%(seasonnumber)02dE%(episodenumber)02d.%(ext)s'
1087 self.config['ep_metadata'] = self.config['with_ep_name']
1088
1089 # The default format of the graphics file names (with and without seasons and/or episode names)
1090 # The default is to use the URL's filename from thetvdb.com
1091 self.config['g_defaultname']=True
1092 # e.g. "Fringe - 01.jpg"
1093 self.config['g_series'] = u'%(series)s - %(seq)s.%(ext)s'
1094 # e.g. "SG-1 - 07-02.jpg"
1095 self.config['g_season'] = u'%(series)s - %(seasonnumber)02d-%(seq)s.%(ext)s'
1096
1097 # Set default configuration variables
1098 # Start - Variables the user can override through option "-u" with their own file of variables
1099 self.config['allgraphicsdir'] = os.getcwd()
1100 self.config['posterdir'] = None
1101 self.config['bannerdir'] = None
1102 self.config['fanartdir'] = None
1103 self.config['episodeimagedir'] = None
1104 self.config['metadatadir'] = None
1105 self.config['mythtvmeta'] = False
1106 self.config['myth_secondary_sources'] = {}
1107 self.config['posterresize'] = False
1108 self.config['fanartresize'] = False
1109 self.config['min_poster_size'] = 400
1110 self.config['image_library'] = False
1111 self.config['ffmpeg'] = True
1112 self.config['folderart'] = False
1113 self.config['metadata_exclude_as_update_trigger'] = ['intid', 'season', 'episode', 'showlevel', 'filename', 'coverfile', 'childid', 'browse', 'playcommand', 'trailer', 'host', 'screenshot', 'banner', 'fanart']
1114 self.config['filename_char_filter'] = u"/%\000"
1115 self.config['ignore-directory'] = []
1116
1117
1118 # Dictionaries for Miro Bridge metadata downlods
1119 self.config['mb_tv_channels'] = {}
1120 self.config['mb_movies'] = {}
1121
1122 # Episode data keys that you want to display or download.
1123 # This includes the order that you want them display or in the downloaded file.
1124 self.config['ep_include_data'] = [u'series', u'seasonnumber', u'episodenumber', u'episodename', u'rating', u'overview', u'director', u'writer', u'cast', u'gueststars', u'imdb_id', u'filename', u'epimgflag', u'language', u'runtime', u'firstaired', u'genres', u'lastupdated', u'productioncode', u'id', u'seriesid', u'seasonid', u'absolute_number', u'combined_season', u'combined_episodenumber', u'dvd_season', u'dvd_discid', u'dvd_chapter', u'dvd_episodenumber']
1125
1126 self.config['config_file'] = False
1127 self.config['series_name_override'] = False
1128 self.config['ep_name_massage'] = False
1129 self.config['video_file_exts'] = [u'3gp', u'asf', u'asx', u'avi', u'mkv', u'mov', u'mp4', u'mpg', u'qt', u'rm', u'swf', u'wmv', u'm2ts', u'ts', u'evo', u'img', u'iso']
1130
1131
1132 # Regex pattern strings used to check for season number from directory names
1133 self.config['season_dir_pattern'] = [
1134 # foo_01 ????
1135 re.compile(u'''^.+?[ \._\-]([0-9]+)[^\\/]*$''', re.UNICODE),
1136 # foo_S01 ????
1137 re.compile(u'''^.+?[ \._\-][Ss]([0-9]+)[^\\/]*$''', re.UNICODE),
1138 # 01 ????
1139 re.compile(u'''([0-9]+)[^\\/]*$''', re.UNICODE),
1140 # s01 ????
1141 re.compile(u'''[Ss]([0-9]+)[^\\/]*$''', re.UNICODE),
1142 ]
1143
1144
1145 # Set default regex pattern strings used to extract series name , season and episode numbers for file name
1146 self.config['name_parse'] = [
1147 # foo_[s01]_[e01]
1148 re.compile(u'''^(.+?)[ \._\-]\[[Ss]([0-9]+?)\]_\[[Ee]([0-9]+?)\]?[^\\/]*$''', re.UNICODE),
1149 # foo.1x09*
1150 re.compile(u'''^(.+?)[ \._\-]\[?([0-9]+)x([0-9]+)[^\\/]*$''', re.UNICODE),
1151 # foo.s01.e01, foo.s01_e01
1152 re.compile(u'''^(.+?)[ \._\-][Ss]([0-9]+)[\.\- ]?[Ee]([0-9]+)[^\\/]*$''' , re.UNICODE),
1153 # foo.103*
1154 re.compile(u'''^(.+)[ \._\-]([0-9]{1})([0-9]{2})[\._ -][^\\/]*$''' , re.UNICODE),
1155 # foo.0103*
1156 re.compile(u'''^(.+)[ \._\-]([0-9]{2})([0-9]{2,3})[\._ -][^\\/]*$''' , re.UNICODE),
1157 ]
1158
1159 # regex strings to parse folder names for TV series title, season and episode numbers
1160 self.config['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': u'Season', u'episode': u'Episode'}
1161 self.config['fullname_parse_regex'] = [
1162 # Title/Season 1/s01e01 Subtitle
1163 u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1164 u'''(?P<seasno>[0-9]+)/[Ss][0-9]+[Ee](?P<epno>[0-9]+).+$''',
1165 # ramsi
1166 # Title/Season 1/1x01 Subtitle
1167 u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1168 u'''[0-9]+/(?P<seasno>[0-9]+)[Xx](?P<epno>[0-9]+).+$''',
1169 # Title/Season 1/01 Subtitle
1170 u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1171 u'''(?P<seasno>[0-9]+)/(?P<epno>[0-9]+).+$''',
1172 # Title/Season 1/Title s01e01 Subtitle
1173 u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1174 u'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s [Ss][0-9]+'''+
1175 u'''[Ee](?P<epno>[0-9]+).+$''',
1176 # Title/Season 1/Title 1x01 Subtitle
1177 u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1178 u'''(?P<seasno>[0-9]+)/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))'''+
1179 u'''[Xx](?P<epno>[0-9]+).+$''',
1180 # Title/Season 1/Episode 1 Subtitle
1181 u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1182 u'''(?P<seasno>[0-9]+)/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1183 # Title/Season 1/Season 1 Episode 1 Subtitle
1184 u'''^.+?/(?P<seriesname>[^/]+)/%(season)s%(slash)s '''+
1185 u'''(?P<seasno>[0-9]+)/%(season)s%(slash)s (?:(?P=seasno))%(slash)s '''+
1186 u'''%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1187 # Title Season 1/01 Subtitle
1188 u'''^.+?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1189 u'''/(?P<epno>[0-9]+).+$''',
1190 # Title Season 1/s01e01 Subtitle
1191 u'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1192 u'''/[Ss][0-9]+[Ee](?P<epno>[0-9]+).+''',
1193 # Title Season 1/1x01 Subtitle
1194 u'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1195 u'''/(?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1196 # Title Season 1/Title s01e01 Subtitle
1197 u'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1198 u'''/(?:(?P=seriesname))%(slash)s [Ss][0-9]+[Ee](?P<epno>[0-9]+).+$''',
1199 # Title Season 1/Title 1x01 Subtitle
1200 u'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1201 u'''/(?:(?P=seriesname))%(slash)s (?:(?P=seasno))[Xx](?P<epno>[0-9]+).+$''',
1202 # Title Season 1/Episode 1 Subtitle
1203 u'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1204 u'''/%(episode)s%(slash)s (?P<epno>[0-9]+).+$''',
1205 # Title Season 1/Season 1 Episode 1 Subtitle
1206 u'''^.*?/(?P<seriesname>[^/]+)%(slash)s %(season)s%(slash)s (?P<seasno>[0-9]+)'''+
1207 u'''/%(season)s%(slash)s (?:(?P=seasno))%(slash)s %(episode)s%(slash)s (?P<epno>[0-9]+).+$'''
1208 ]
1209
1210 # Initalize a valriable used by the -MW option
1211 self.program_seriesid = None
1212 self.config[u'file_move_flag'] = False
1213
1214 # end __init__
1215
1216 # Local variable
1217 data_flags_table={ 'P': 'get_poster', 'B': 'get_banner', 'F': 'get_fanart', 'I': 'get_ep_image', 'E': 'get_ep_meta'}
1218
1219
1220 def _initLogger(self):
1221 """Sets up a logger using the logging module, returns a log object
1222 """
1223 logger = logging.getLogger(u"jamu")
1224 formatter = logging.Formatter(u'%(asctime)s) %(levelname)s %(message)s')
1225
1226 hdlr = logging.StreamHandler(sys.stdout)
1227
1228 hdlr.setFormatter(formatter)
1229 logger.addHandler(hdlr)
1230
1231 if self.config['debug_enabled']:
1232 logger.setLevel(logging.DEBUG)
1233 else:
1234 logger.setLevel(logging.WARNING)
1235 return logger
1236 #end initLogger
1237
1238 def setUseroptions(self, useroptions):
1239 """ Change variables through a user supplied configuration file
1240 return False and exit the script if there are issues with the configuration file values
1241 """
1242 if useroptions[0]=='~':
1243 useroptions=os.path.expanduser("~")+useroptions[1:]
1244 if os.path.isfile(useroptions) == False:
1245 sys.stderr.write(
1246 "\n! Error: The specified user configuration file (%s) is not a file\n" % useroptions
1247 )
1248 sys.exit(1)
1249 cfg = ConfigParser.SafeConfigParser()
1250 cfg.readfp(codecs.open(useroptions, "r", "utf8"))
1251 for section in cfg.sections():
1252 if section[:5] == 'File ':
1253 self.config['config_file'] = section[5:]
1254 continue
1255 if section == 'variables':
1256 # Change variables per user config file
1257 for option in cfg.options(section):
1258 if option == 'video_file_exts' or option == 'tmdb_genre_filter' or option == 'metadata_exclude_as_update_trigger':
1259 tmp_list = (cfg.get(section, option).rstrip()).split(',')
1260 for i in range(len(tmp_list)): tmp_list[i] = (tmp_list[i].strip()).lower()
1261 self.config[option] = tmp_list
1262 continue
1263 if option == 'filename_char_filter':
1264 for char in cfg.get(section, option):
1265 self.config['filename_char_filter']+=char
1266 continue
1267 if option == 'translate':
1268 s_e = (cfg.get(section, option).rstrip()).split(',')
1269 if not len(s_e) == 2:
1270 continue
1271 for index in range(len(s_e)):
1272 s_e[index] = s_e[index].strip()
1273 self.config['fullname_parse_season_episode_translation'] = {u'slash': u'\\', u'season': s_e[0], u'episode': s_e[1]}
1274 continue
1275
1276 # Ignore user settings for Myth Video and graphics file directories
1277 # when the MythTV metadata option (-M) is selected
1278 if self.config['mythtvmeta'] and option in ['posterdir', 'bannerdir', 'fanartdir', 'episodeimagedir', 'mythvideo']:
1279 continue
1280 self.config[option] = cfg.get(section, option)
1281 continue
1282 if section == 'regex':
1283 # Change variables per user config file
1284 for option in cfg.options(section):
1285 self.config['name_parse'].append(re.compile(cfg.get(section, option), re.UNICODE))
1286 continue
1287 if section == 'ignore-directory':
1288 # Video directories to be excluded from Jamu processing
1289 for option in cfg.options(section):
1290 self.config['ignore-directory'].append(cfg.get(section, option))
1291 continue
1292 if section =='series_name_override':
1293 overrides = {}
1294 for option in cfg.options(section):
1295 overrides[option] = cfg.get(section, option)
1296 if len(overrides) > 0:
1297 self.config['series_name_override'] = overrides
1298 continue
1299 if section =='ep_name_massage':
1300 massage = {}
1301 for option in cfg.options(section):
1302 tmp =cfg.get(section, option).split(',')
1303 if len(tmp)%2 and len(cfg.get(section, option)) != 0:
1304 sys.stderr.write(u"\n! Error: For (%s) 'ep_name_massage' values must be in pairs\n" % option)
1305 sys.exit(1)
1306 tmp_array=[]
1307 i=0
1308 while i != len(tmp):
1309 tmp[i] = tmp[i].strip()
1310 tmp[i+1] = tmp[i+1].strip()
1311 tmp_array.append([tmp[i].replace('"',''), tmp[i+1].replace('"','')])
1312 i+=2
1313 massage[option]=tmp_array
1314 if len(massage) > 0:
1315 self.config['ep_name_massage'] = massage
1316 continue
1317 if section == 'ep_metadata_to_download':
1318 if len(cfg.options(section)):
1319 if cfg.options(section)[0] == 'ep_include_data':
1320 tmp=cfg.get(section, cfg.options(section)[0])
1321 overrides=tmp.split(',')
1322 for index in range(len(overrides)):
1323 x = overrides[index].replace(' ','')
1324 if len(x) != 0:
1325 overrides[index]=x
1326 else:
1327 del overrides[index]
1328 self.config['ep_include_data']=overrides
1329 continue
1330 if section == 'data_flags':
1331 if len(cfg.options(section)):
1332 for option in cfg.options(section):
1333 if cfg.get(section, option).lower() != 'False'.lower():
1334 for key in self.data_flags_table.keys():
1335 if option == self.data_flags_table[key]:
1336 self.config[option] = True
1337 continue
1338 for sec in ['movies-secondary-sources', 'tv-secondary-sources']:
1339 if section == sec:
1340 secondary = {}
1341 for option in cfg.options(section):
1342 secondary[option] = cfg.get(section, option)
1343 if len(secondary) > 0:
1344 self.config['myth_secondary_sources'][sec[:sec.index('-')]] = secondary
1345 continue
1346 if section == u'mb_tv':
1347 # Add the channel names and their corresponding thetvdb.com id numbers
1348 for option in cfg.options(section):
1349 self.config['mb_tv_channels'][filter(is_not_punct_char, option.lower())] = [cfg.get(section, option), u'']
1350 continue
1351 if section == u'mb_movies':
1352 # Add the channel names for movie trailer Channels
1353 for option in cfg.options(section):
1354 self.config['mb_movies'][filter(is_not_punct_char, option.lower())] = cfg.get(section, option)
1355 continue
1356
1357 # Expand any home directories that are not fully qualified
1358 dirs_to_check= [u'bannerdir', u'episodeimagedir', u'metadatadir', u'posterdir', u'video_dir', u'fanartdir']
1359 for item in dirs_to_check:
1360 if self.config[item]:
1361 if item == u'metadatadir' and not self.config[item]:
1362 continue
1363 if self.config[item][0]=='~':
1364 self.config[item]=os.path.expanduser("~")+self.config[item][1:]
1365 # end setUserconfig
1366
1367 def displayOptions(self):
1368 """ Display all of the configuration values. This is used to verify that the user has the
1369 variables set as they want before running jamu live.
1370 """
1371 keys=self.config.keys()
1372 keys.sort()
1373
1374 ################### Used to create the example configuration file "jamu-example-conf"
1375 # for key in keys: # Used to create the example configuration file "jamu-example-conf"
1376 # print "#%s: %s" % (key, self.config[key])
1377 # sys.exit(0)
1378 ##################
1379
1380 for key in keys:
1381 if key == 'log': # Do not display the logger instance it is irrelevant for display
1382 continue
1383 try:
1384 if key == 'name_parse':
1385 print u"%s (%d items)" % (key, len(self.config[key]))
1386 else:
1387 print u"%s (%s)" % (key, str(self.config[key]))
1388 except:
1389 try:
1390 print u"%s (%d items)" % (key, len(self.config[key]))
1391 except:
1392 print u"%s:" % key, self.config[key]
1393 # end set_Userconfig
1394
1395 def changeVariable(self, key, value):
1396 """Change any configuration variable - caution no validation is preformed
1397 """
1398 self.config[key]=value
1399 # end changeVariable
1400
1401
1402 def _checkNFS(self, dirs, ext_filter):
1403 '''Check if any of the files are on NFS shares. If they are then the user must be warned.
1404 return True if there are at least one file is on a NFS share.
1405 return False if no graphic files are on an NFS share.
1406 '''
1407 tmp_dirs = []
1408 for d in dirs: # Get rid of Null directories
1409 if d:
1410 tmp_dirs.append(d)
1411 dirs = tmp_dirs
1412
1413 global localhostname, graphicsDirectories
1414 try:
1415 localip = gethostbyname(localhostname) # Get the local hosts IP address
1416 except Exception, e:
1417 sys.stderr.write("\n! Error: There is no valid address-to-host mapping for the host (%s)\nThe Jamu Janitor (-MJ) option cannot be used while this issue remains un-resolved.\nError:(%s)\n" % (localhostname, e))
1418 sys.exit(1)
1419
1420 # Get all curently mounted NFS shares
1421 tmp_mounts = callCommandLine("mount -l | grep '//'").split('\n')
1422 nfs = []
1423 for mount in tmp_mounts:
1424 mount.rstrip()
1425 parts = mount.split(' ')
1426 tmparray=[P for P in parts]
1427 if tmparray[0].startswith('//'): # Is this a NFS share definition
1428 if not tmparray[0].startswith(u'//%s' % localip) and not tmparray[0].startswith(u'//%s' % localhostname):
1429 nfs.append(tmparray[2]) # Add an NFS mount name
1430
1431 if not len(nfs): # Check if there are any NFS mounts
1432 return False
1433
1434 # Check if any of the directories have files on an NFS share
1435 for directory in dirs: # Check the base directories first
1436 for mount in nfs:
1437 if os.path.realpath(directory).startswith(mount):
1438 return True
1439 for directory in dirs: # Check the actual files
1440 file_list = _getFileList([directory])
1441 if not len(file_list):
1442 continue
1443 tmp_list = []
1444 for fle in file_list: # Make a copy of file_list
1445 tmp_list.append(fle)
1446 for g_file in tmp_list: # Cull the list removing dirs and non-extention files
1447 if os.path.isdir(g_file):
1448 file_list.remove(g_file)
1449 continue
1450 g_ext = _getExtention(g_file)
1451 if not g_ext.lower() in ext_filter:
1452 file_list.remove(g_file)
1453 continue
1454 for filename in file_list: # Actually check each file against the NFS mounts
1455 for mount in nfs:
1456 if os.path.realpath(filename).startswith(mount):
1457 return True
1458 return False
1459 # end _checkNFS
1460
1461
1462 def _getMythtvDirectories(self):
1463 """Get all graphics directories found in the MythTV DB and change their corresponding
1464 configuration values. /media/video:/media/virtual/VB_Share/Review
1465 """
1466 # Stop processing if this local host has any storage groups
1467 global localhostname, storagegroups
1468 # Make sure Jamu is being run on a MythTV backend
1469 if not mythbeconn:
1470 sys.stderr.write(u"\n! Error: Jamu must be run on a MythTV backend. Local host (%s) is not a MythTV backend.\n" % localhostname)
1471 sys.exit(1)
1472
1473 global dir_dict
1474 for key in dir_dict.keys():
1475 graphics_dir = mythdb.settings[localhostname][dir_dict[key]]
1476 # Only use path from MythTV if one was found
1477 self.config[key] = []
1478 if key == 'mythvideo' and graphics_dir:
1479 tmp_directories = graphics_dir.split(':')
1480 if len(tmp_directories):
1481 for i in range(len(tmp_directories)):
1482 tmp_directories[i] = tmp_directories[i].strip()
1483 if tmp_directories[i] != '':
1484 if os.access(tmp_directories[i], os.F_OK):
1485 self.config[key].append(tmp_directories[i])
1486 continue
1487 else:
1488 sys.stderr.write(u"\n! Warning: MythTV video directory (%s) does not exist.\n" % (tmp_directories[i]))
1489 continue
1490
1491 if key != 'mythvideo' and graphics_dir:
1492 if os.path.os.access(graphics_dir, os.F_OK):
1493 self.config[key] = [graphics_dir]
1494 else:
1495 sys.stderr.write(u"\n! Warning: MythTV (%s) directory (%s) does not exist.\n" % (key, graphics_dir))
1496
1497 # Save the FE path settings local to this backend
1498 self.config['localpaths'] = {}
1499 for key in dir_dict.keys():
1500 self.config['localpaths'][key] = []
1501 local_paths = []
1502 if len(self.config[key]):
1503 self.config['localpaths'][key] = list(self.config[key])
1504
1505 # If there is a Videos SG then there is always a Graphics SG using Videos as a fallback
1506 getStorageGroups()
1507 for key in dir_dict.keys():
1508 if key == 'episodeimagedir' or key == 'mythvideo':
1509 continue
1510 if storagegroups.has_key(u'mythvideo') and not storagegroups.has_key(key):
1511 storagegroups[key] = list(storagegroups[u'mythvideo']) # Set fall back
1512
1513 # Use Storage Groups as the priority but append any FE directory settings that
1514 # are local to this BE but are not already used as a storage group
1515 if storagegroups.has_key(u'mythvideo'):
1516 for key in storagegroups.keys():
1517 self.config[key] = list(storagegroups[key])
1518 for k in self.config['localpaths'][key]:
1519 if not k in self.config[key]:
1520 self.config[key].append(k) # Add any FE settings local directories not already included
1521 else:
1522 if key == 'mythvideo':
1523 sys.stdout.write(u"\n! Warning: You have a front end video directory path that is a duplicate of this backend's 'Videos' storage group.\nFront end directory (%s)\nThe Front end setting has been ignored.\nThis Front end video directory will cause duplicate entires in MythVideo.\n" % (k))
1524 else:
1525 sys.stdout.write(u"\n! Info: You have a front end directory path that is a duplicate of this backend's storage group.\nFront end directory (%s)\nThe Front end setting has been ignored.\n" % (k))
1526 continue
1527
1528 # Make sure there is a directory set for Videos and other graphics directories on this host
1529 exists = True
1530 for key in dir_dict.keys():
1531 if key == 'episodeimagedir': # Jamu does nothing with Screenshots
1532 continue
1533 # The fall back graphics SG is the Videos SG directory as of changeset 22104
1534 if storagegroups.has_key(u'mythvideo') and not len(self.config[key]):
1535 self.config[key] = storagegroups[u'mythvideo']
1536 if not len(self.config[key]):
1537 sys.stderr.write(u"\n! Error: There must be a directory for Videos and each graphic type. The (%s) directory is missing.\n" % (key))
1538 exists = False
1539 if not exists:
1540 sys.exit(1)
1541
1542 # Make sure that the directory set for Videos and other graphics directories have the proper permissions
1543 accessable = True
1544 for key in dir_dict.keys():
1545 for directory in self.config[key]:
1546 if key == 'episodeimagedir': # Jamu does nothing with Screenshots
1547 continue
1548 if key == 'mythvideo':
1549 if not os.access(directory, os.F_OK | os.R_OK):
1550 sys.stderr.write(u"\n! Error: This video directory must have read access for Jamu to function.\nThere is a permissions issue with (%s).\n" % (directory, ))
1551 accessable = False
1552 continue
1553 if not os.access(directory, os.F_OK | os.R_OK | os.W_OK):
1554 sys.stderr.write(u"\n! Error: The (%s) directory (%s) must be read/writable for Jamu to function.\n" % (key, directory, ))
1555 accessable = False
1556 if not accessable:
1557 sys.exit(1)
1558
1559 # Print out the video and image directories that will be used for processing
1560 if self.config['mythtv_verbose']:
1561 dir_types={'posterdir': "Cover art ", 'bannerdir': 'Banners ', 'fanartdir': 'Fan art ', 'episodeimagedir': 'Screenshots', 'mythvideo': 'Video '}
1562 sys.stdout.write(u"\n==========================================================================================\n")
1563 sys.stdout.write(u"Listed below are the types and base directories Jamu will use for processing.\nThe list reflects your current configuration for the '%s' back end\nand whether a directory is a 'SG' (storage group) or not.\n" % localhostname)
1564 sys.stdout.write(u"Note: All directories are from settings in the MythDB specific to hostname (%s).\n" % localhostname)
1565 sys.stdout.write(u"Note: Screenshot directories are not listed as Jamu does not process Screenshots.\n")
1566 sys.stdout.write(u"------------------------------------------------------------------------------------------\n")
1567 for key in dir_dict.keys():
1568 if key == 'episodeimagedir':
1569 continue
1570 for directory in self.config[key]:
1571 sg_flag = 'NO '
1572 if storagegroups.has_key(key):
1573 if directory in storagegroups[key]:
1574 sg_flag = 'YES'
1575 sys.stdout.write(u"Type: %s - SG-%s - Directory: (%s)\n" % (dir_types[key], sg_flag, directory))
1576 sys.stdout.write(u"------------------------------------------------------------------------------------------\n")
1577 sys.stdout.write(u"If a directory you set from a separate Front end is not displayed it means\nthat the directory is not accessible from this backend OR\nyou must add the missing directories using the Front end on this Back end.\nFront end settings are host machine specific.\n")
1578 sys.stdout.write(u"==========================================================================================\n\n")
1579
1580 if self.config[u'file_move_flag']: # verify the destination directory in a move is read/writable
1581 index = 0
1582 accessable = True
1583 for arg in self.args:
1584 if index % 2 == 0:
1585 index+=1
1586 continue
1587 if not os.access(arg, os.F_OK):
1588 for dirct in self.config['mythvideo']:
1589 if arg.startswith(dirct):
1590 if not os.access(dirct, os.F_OK | os.R_OK | os.W_OK):
1591 sys.stderr.write(u"! Error: Your move destination root MythVideo directory (%s) must be read/writable for Jamu to function.\n\n" % (dirct, ))
1592 accessable = False
1593 break
1594 else:
1595 sys.stderr.write(u"! Error: Your move destination directory (%s) must be a MythVideo directory OR a subdirectory of a MythVideo directory.\n\n" % (arg, ))
1596 accessable = False
1597 elif not os.access(arg, os.F_OK | os.R_OK | os.W_OK):
1598 sys.stderr.write(u"! Error: Your move destination directory (%s) must be read/writable for Jamu to function.\n\n" % (arg, ))
1599 accessable = False
1600 index+=1
1601 if not accessable:
1602 sys.exit(1)
1603
1604 # Check if any Video files are on a NFS shares
1605 if not self.config['mythtvNFS']: # Maybe the NFS check is to be skipped
1606 if self._checkNFS(self.config['mythvideo'], self.config['video_file_exts']):
1607 sys.stderr.write(u"\n! Error: Your video files reside on a NFS mount.\nIn the case where you have more than one MythTV backend using the same directories to store either video files\nor graphics any Jamu's option (-M) can adversly effect your MythTV database by mistakenly adding videos\nfor other backends or with the Janitor (-J) option mistakenly remove graphics files.\n\nIf you only have one backend or do not mix the Video or graphic file directories between backends and still want to use\nJamu add the options (N) to your option string e.g. (-MJN), which will skip this check.\n\n")
1608 sys.exit(1)
1609 # end _getMythtvDirectories
1610
1611
1612 def _JanitorConflicts(self):
1613 '''Verify that there are no conflict between the graphics directories of MythVideo and
1614 other MythTV plugins. Write an warning message if a conflict is found.
1615 return True when there is a conflict
1616 return False when there is no conflict
1617 '''
1618 # Except for the plugins below no other plugins have non-theme graphics
1619 # MythGallery:
1620 # Table 'settings' fields 'GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation'
1621 # MythGame:
1622 # Table 'settings' fields 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir'
1623 # MythMusic:
1624 # Table 'settings' fields 'MusicLocation'
1625 global graphicsDirectories, localhostname
1626 tablefields = ['GalleryDir', 'GalleryImportDirs', 'GalleryThumbnailLocation', 'mythgame.screenshotDir', 'mythgame.fanartDir', 'mythgame.boxartDir', 'MusicLocation', 'ScreenShotPath']
1627 returnvalue = False # Initalize as no conflicts
1628 for field in tablefields:
1629 tmp_setting = mythdb.settings[localhostname][field]
1630 if not tmp_setting:
1631 continue
1632 settings = tmp_setting.split(':') # Account for multiple dirs per setting
1633 if not len(settings):
1634 continue
1635 for setting in settings:
1636 for directory in graphicsDirectories.keys():
1637 if not self.config[graphicsDirectories[directory]]:
1638 continue
1639 # As the Janitor processes subdirectories matching must be a starts with check
1640 for direc in self.config[graphicsDirectories[directory]]:
1641 if os.path.realpath(setting).startswith(os.path.realpath(direc)):
1642 sys.stderr.write(u"\n! Error - The (%s) directory (%s) conflicts\nwith the MythVideo (%s) directory (%s).\nThe Jamu Janitor (-MJ) option cannot be used.\n\n" % (field, setting, direc, self.config[graphicsDirectories[directory]]) )
1643 returnvalue = True
1644 return returnvalue
1645 # end _JanitorConflicts
1646
1647
1648 def _addMythtvUserFileTypes(self):
1649 """Add video file types to the jamu list from the "videotypes" table
1650 """
1651 # Get videotypes table field names:
1652 try:
1653 records = VideoTypes.getAllEntries(mythdb)
1654 except MythError, e:
1655 sys.stderr.write(u"\n! Error: Reading videotypes MythTV table: %s\n" % e.args[0])
1656 return False
1657
1658 for record in records:
1659 # Remove any extentions that are in Jamu's list but the user wants ignore
1660 if record.f_ignore:
1661 if record.extension in self.config['video_file_exts']:
1662 self.config['video_file_exts'].remove(record.extension)
1663 if record.extension.lower() in self.config['video_file_exts']:
1664 self.config['video_file_exts'].remove(record.extension.lower())
1665 else: # Add extentions that are not in the Jamu list
1666 if not record.extension in self.config['video_file_exts']:
1667 self.config['video_file_exts'].append(record.extension)
1668 # Make sure that all video file extensions are lower case
1669 for index in range(len(self.config['video_file_exts'])):
1670 self.config['video_file_exts'][index] = self.config['video_file_exts'][index].lower()
1671 # end _addMythtvUserFileTypes()
1672
1673
1674 def validate_setVariables(self, args):
1675 """Validate the contents of specific configuration variables
1676 return False and exit the script if an invalid configuation value is found
1677 """
1678 # Fix all variables which were changed by a users configuration files
1679 # to 'None', 'False' and 'True' literals back to their intended values
1680 keys=self.config.keys()
1681 types={'None': None, 'False': False, 'True': True}
1682 for key in keys:
1683 for literal in types.keys():
1684 if self.config[key] == literal:
1685 self.config[key] = types[literal]
1686
1687 # Compile regex strings to parse folder names for TV series title, season and episode numbers
1688 self.config['fullname_parse'] = []
1689 for index in range(len(self.config['fullname_parse_regex'])):
1690 self.config['fullname_parse'].append(re.compile(self.config['fullname_parse_regex'][index] % self.config['fullname_parse_season_episode_translation'], re.UNICODE))
1691
1692 if self.config['mythtvmeta']:
1693 if mythdb == None or mythvideo == None:
1694 sys.stderr.write(u"\n! Error: The MythTV python interface is not installed or Cannot connect to MythTV Backend. MythTV meta data cannot be updated\n\n")
1695 sys.exit(1)
1696 try:
1697 import Image
1698 self.config['image_library'] = Image
1699 except Exception, e:
1700 sys.stderr.write(u"""\n! Error: Python Imaging Library is required for figuring out the sizes of
1701 the fetched poster images.
1702
1703 In Debian/Ubuntu it is packaged as 'python-imaging'.
1704 http://www.pythonware.com/products/pil/\nError:(%s)\n""" % e)
1705 sys.exit(1)
1706
1707 if not _can_int(self.config['min_poster_size']):
1708 sys.stderr.write(u"\n! Error: The poster minimum value must be an integer (%s)\n" % self.config['min_poster_size'])
1709 sys.exit(1)
1710 else:
1711 self.config['min_poster_size'] = int(self.config['min_poster_size'])
1712
1713 if self.config['maximum'] != None:
1714 if _can_int(self.config['maximum']) == False:
1715 sys.stderr.write(u"\n! Error: Maximum option is not an integer (%s)\n" % self.config['maximum'])
1716 sys.exit(1)
1717
1718 # Detect if this is a move request
1719 self.config[u'file_move_flag'] = False
1720 if len(args) != 0:
1721 if os.path.isfile(args[0]) or os.path.isdir(args[0]) or args[0][-1:] == '*':
1722 self.config[u'file_move_flag'] = True
1723 self.args = list(args)
1724
1725 if self.config['mythtvdir']:
1726 if mythdb == None or mythvideo == None:
1727 sys.stderr.write(u"\n! Error: MythTV python interface is not available\n")
1728 sys.exit(1)
1729 if self.config['mythtvdir'] or self.config['mythtvmeta']:
1730 self._addMythtvUserFileTypes() # add user filetypes from the "videotypes" table
1731 self._getMythtvDirectories()
1732 if self.config['mythtvjanitor']: # Check for graphic directory conflicts with other plugins
1733 if self._JanitorConflicts():
1734 sys.exit(1)
1735 if not self.config['mythtvNFS']:
1736 global graphicsDirectories, image_extensions
1737 dirs = []
1738 for key in graphicsDirectories:
1739 if key != u'screenshot':
1740 for directory in self.config[graphicsDirectories[key]]:
1741 dirs.append(directory)
1742 # Check if any Graphics files are on NFS shares
1743 if self._checkNFS(dirs, image_extensions):
1744 sys.stderr.write(u"\n! Error: Your metadata graphics reside on a NFS mount.\nIn the case where you have more than one MythTV backend using the same directories to store your graphics\nthe Jamu's Janitor option (-MJ) will be destructive removing graphics used by the other backend(s).\n\nIf you only have one backend or do not mix the graphics directories between backends and still want to use\nJamu's Janitor use the options (-MJN) which will skip this check.\n\n")
1745 sys.exit(1)
1746
1747 if self.config['posterresize'] != False or self.config['fanartresize'] != False:
1748 if _useImageMagick("-version"):
1749 sys.stderr.write(u"\n! Error: ImageMagick is not installed, graphics cannot be resized. posterresize(%s), fanartresize(%s)\n" % (str(self.config['posterresize']), str(self.config['fanartresize'])))
1750 sys.exit(1)
1751
1752 if self.config['mythtvmeta'] and len(args) == 0:
1753 args=['']
1754
1755 if len(args) == 0:
1756 sys.stderr.write(u"\n! Error: At least a video directory, SID or season name must be supplied\n")
1757 sys.exit(1)
1758
1759 if os.path.isfile(args[0]) or os.path.isdir(args[0]) or args[0][-1:] == '*':
1760 self.config['video_dir'] = []
1761 for arg in args:
1762 self.config['video_dir'].append(unicode(arg,'utf8'))
1763 elif not self.config['mythtvmeta']:
1764 if _can_int(args[0]) and len(args[0]) >= 5:
1765 self.config['sid'] = unicode(args[0], 'utf8') # There is still a chance that this is a series name "90210"
1766 else:
1767 if self.config['series_name_override']:
1768 if self.config['series_name_override'].has_key(args[0].lower()):
1769 self.config['sid'] = unicode((self.config['series_name_override'][args[0].lower()]).strip(), 'utf8')
1770 else:
1771 self.config['series_name'] = unicode(args[0].strip(), 'utf8')
1772 else:
1773 self.config['series_name'] = unicode(args[0].strip(), 'utf8')
1774 if len(args) != 1:
1775 if len(args) > 3:
1776 sys.stderr.write("\n! Error: Too many arguments (%d), maximum is three.\n" % len(args))
1777 print "! args:", args
1778 sys.exit(1)
1779 if len(args) == 3 and _can_int(args[1]) and _can_int(args[2]):
1780 self.config['season_num'] = args[1]
1781 self.config['episode_num'] = args[2]
1782 elif len(args) == 3:
1783 sys.stderr.write(u"\n! Error: Season name(%s), season number(%s), episode number (%s) combination is invalid\n" % (args[0], args[1], args[2]))
1784 sys.exit(1)
1785 elif len(args) == 2 and _can_int(args[1]):
1786 self.config['season_num'] = args[1]
1787 else:
1788 if self.config['ep_name_massage']:
1789 if self.config['ep_name_massage'].has_key(self.config['series_name']):
1790 tmp_ep_name=args[1].strip()
1791 tmp_array=self.config['ep_name_massage'][self.config['series_name']]
1792 for pair in tmp_array:
1793 tmp_ep_name = tmp_ep_name.replace(pair[0],pair[1])
1794 self.config['episode_name'] = unicode(tmp_ep_name, 'utf8')
1795 else:
1796 self.config['episode_name'] = unicode(args[1].strip(), 'utf8')
1797 else:
1798 self.config['episode_name'] = unicode(args[1].strip(), 'utf8')
1799
1800 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
1801 # Hard-coded here as it is realtively static, and saves another HTTP request, as
1802 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
1803 valid_languages = ["da", "fi", "nl", "de", "it", "es", "fr","pl", "hu","el","tr", "ru","he","ja","pt","zh","cs","sl", "hr","ko","en","sv","no"]
1804
1805 # Validate language as specified by user
1806 if self.config['local_language']:
1807 if not self.config['local_language'] in valid_languages:
1808 valid_langs = ''
1809 for lang in valid_languages: valid_langs+= lang+', '
1810 valid_langs=valid_langs[:-2]
1811 sys.stderr.write(u"\n! Error: Specified language(%s) must match one of the following languages supported by thetvdb.com wiki:\n (%s)\n" % (self.config['local_language'], valid_langs))
1812 sys.exit(1)
1813 global UI_search_language
1814 UI_search_language = self.config['local_language']
1815
1816 if self.config['data_flags']:
1817 for data_type in self.config['data_flags']:
1818 if self.data_flags_table.has_key(data_type):
1819 self.config[self.data_flags_table[data_type]]=True
1820 # end validate_setVariables
1821
1822 def __repr__(self):
1823 """Return a copy of the configuration variables
1824 """
1825 return self.config
1826 #end __repr__
1827 # end class Configuration
1828
1829
1830 class Tvdatabase(object):
1831 """Process direct thetvdb.com requests
1832 """
1833 def __init__(self, configuration):
1834 """Retrieve all configuration options and get an instance of tvdb_api which is used to
1835 access thetvdb.com wiki.
1836 """
1837 self.config = configuration
1838 cache_dir=u"/tmp/tvdb_api_%s/" % os.geteuid()
1839 if self.config['interactive']:
1840 self.config['tvdb_api'] = tvdb_api.Tvdb(banners=True, debug=self.config['debug_enabled'], interactive=True, select_first=False, cache=cache_dir, actors = True, language = self.config['local_language'], custom_ui=jamu_ConsoleUI, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV)
1841 else:
1842 self.config['tvdb_api'] = tvdb_api.Tvdb(banners=True, debug = self.config['debug_enabled'], cache = cache_dir, actors = True, language = self.config['local_language'], apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV)
1843
1844 # Local variables
1845 # High level dictionay keys for select graphics URL(s)
1846 fanart_key=u'fanart'
1847 banner_key=u'series'
1848 poster_key=u'poster'
1849 season_key=u'season'
1850 # Lower level dictionay keys for select graphics URL(s)
1851 poster_series_key=u'680x1000'
1852 poster_season_key=u'season'
1853 fanart_hires_key=u'1920x1080'
1854 fanart_lowres_key=u'1280x720'
1855 banner_series_key=u'graphical'
1856 banner_season_key=u'seasonwide'
1857 # Type of graphics being requested
1858 poster_type=u'poster'
1859 fanart_type=u'fanart'
1860 banner_type=u'banner'
1861 ep_image_type=u'filename'
1862
1863 def sanitiseFileName(self, name):
1864 '''Take a file name and change it so that invalid or problematic characters are substituted with a "_"
1865 return a sanitised valid file name
1866 '''
1867 if name == None or name == u'':
1868 return u'_'
1869 for char in self.config['filename_char_filter']:
1870 name = name.replace(char, u'_')
1871 if name[0] == u'.':
1872 name = u'_'+name[1:]
1873 return name
1874 # end sanitiseFileName()
1875
1876
1877 def _getSeriesBySid(self, sid):
1878 """Lookup a series via it's sid
1879 return tvdb_api Show instance
1880 """
1881 seriesid = u'sid:' + sid
1882 if not self.corrections.has_key(seriesid):
1883 self._getShowData(sid)
1884 self.corrections[seriesid] = sid
1885 return self.shows[sid]
1886 tvdb_api.Tvdb.series_by_sid = _getSeriesBySid
1887 # end _getSeriesBySid
1888
1889 def _searchforSeries(self, sid_or_name):
1890 """Get TV series data by sid or series name
1891 return None if the TV show was not found
1892 return an tvdb_api instance of the TV show data if it was found
1893 """
1894 if self.config['sid']:
1895 show = self.config['tvdb_api'].series_by_sid(self.config['sid'])
1896 if len(show) != 0:
1897 self.config['series_name']=show[u'seriesname']
1898 return show
1899 else:
1900 if self.config['series_name_override']:
1901 if self.config['series_name_override'].has_key(sid_or_name.lower()):
1902 self.config['sid'] = (self.config['series_name_override'][sid_or_name.lower()])
1903 show = self.config['tvdb_api'].series_by_sid(self.config['sid'])
1904 if len(show) != 0:
1905 self.config['series_name'] = show[u'seriesname']
1906 return show
1907 else:
1908 show = self.config['tvdb_api'][sid_or_name]
1909 if len(show) != 0:
1910 self.config['series_name'] = show[u'seriesname']
1911 return show
1912 else:
1913 show = self.config['tvdb_api'][sid_or_name]
1914 if len(show) != 0:
1915 self.config['series_name'] = show[u'seriesname']
1916 return show
1917 # end _searchforSeries
1918
1919 def verifySeriesExists(self):
1920 """Verify that a:
1921 Series or
1922 Series and Season or
1923 Series and Season and Episode number or
1924 Series and Episode name
1925 passed by the user exists on thetvdb.com
1926 return False and display an appropriate error if the TV data was not found
1927 return an tvdb_api instance of the TV show/season/episode data if it was found
1928 """
1929 sid=self.config['sid']
1930 series_name=self.config['series_name']
1931 season=self.config['season_num']
1932 episode=self.config['episode_num']
1933 episode_name=self.config['episode_name']
1934 try:
1935 self.config['log'].debug(u'Checking for series(%s), sid(%s), season(%s), episode(%s), episode name(%s)' % (series_name, sid, season, episode, episode_name))
1936 if episode_name: # Find an exact match for the series and episode name
1937 series_sid=''
1938 if sid:
1939 seriesfound=self._searchforSeries(sid).search(episode_name)
1940 else:
1941 seriesfound=self._searchforSeries(series_name).search(episode_name)
1942 if len(seriesfound) != 0:
1943 for ep in seriesfound:
1944 if ep['seriesid'] == '999999999':
1945 self.config['sid'] = ep['seriesid']
1946 return(ep)
1947 if (ep['episodename'].lower()).startswith(episode_name.lower()):
1948 if len(ep['episodename']) > (len(episode_name)+1):
1949 # Skip episodes the are not part of a set of (1), (2) ... etc
1950 if ep['episodename'][len(episode_name):len(episode_name)+2] != ' (':
1951 continue
1952 series_sid = ep['seriesid']
1953 self.config['sid'] = ep['seriesid']
1954 self.config['season_num'] = ep['seasonnumber']
1955 self.config['episode_num'] = ep['episodenumber']
1956 return(ep)
1957 else: # Exact match
1958 series_sid = ep['seriesid']
1959 self.config['sid'] = ep['seriesid']
1960 self.config['season_num'] = ep['seasonnumber']
1961 self.config['episode_num'] = ep['episodenumber']
1962 return(ep)
1963 raise tvdb_episodenotfound
1964 # Search for the series or series & season or series & season & episode
1965 elif season:
1966 if episode: # series & season & episode
1967 seriesfound=self._searchforSeries(series_name)[int(season)][int(episode)]
1968 if seriesfound['seriesid'] == '999999999':
1969 return(seriesfound)
1970 self.config['sid'] = seriesfound['seriesid']
1971 self.config['episode_name'] = seriesfound['episodename']
1972 else: # series & season
1973 seriesfound=self._searchforSeries(series_name)[int(season)]
1974 else:
1975 seriesfound=self._searchforSeries(series_name) # Series only
1976 except tvdb_shownotfound:
1977 # No such show found.
1978 # Use the show-name from the files name, and None as the ep name
1979 if series_name:
1980 sys.stderr.write(u"\n! Warning: Series (%s) not found\n" % (
1981 series_name )
1982 )
1983 else:
1984 sys.stderr.write(u"\n! Warning: Series TVDB number (%s) not found\n" % (
1985 sid )
1986 )
1987 return(False)
1988 except (tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound):
1989 # The season, episode or name wasn't found, but the show was.
1990 # Use the corrected show-name, but no episode name.
1991 if series_name == None:
1992 series_name = sid
1993 if episode:
1994 sys.stderr.write(u"\n! Warning: For Series (%s), season (%s) or Episode (%s) not found \n"
1995 % (series_name, season, episode )
1996 )
1997 elif episode_name:
1998 sys.stderr.write(u"\n! Warning: For Series (%s), Episode (%s) not found \n"
1999 % (series_name, episode_name )
2000 )
2001 else:
2002 sys.stderr.write(u"\n! Warning: For Series (%s), season (%s) not found \n" % (
2003 series_name, season)
2004 )
2005 return(False)
2006 except tvdb_error, errormsg:
2007 # Error communicating with thetvdb.com
2008 if sid: # Maybe the 5 digit number was a series name (e.g. 90210)
2009 self.config['series_name']=self.config['sid']
2010 self.config['sid'] = None
2011 return self.verifySeriesExists()
2012 sys.stderr.write(
2013 u"\n! Warning: Error contacting www.thetvdb.com:\n%s\n" % (errormsg)
2014 )
2015 return(False)
2016 except tvdb_userabort, errormsg:
2017 # User aborted selection (q or ^c)
2018 print "\n", errormsg
2019 return(False)
2020 else:
2021 return(seriesfound)
2022 # end verifySeriesExists
2023
2024 def _resizeGraphic(self, filename, resize):
2025 """Resize a graphics file
2026 return False and display an error message if the graphics resizing failed
2027 return True if the resize was succcessful
2028 """
2029 if self.config['simulation']:
2030 sys.stdout.write(
2031 u"Simulation resize command (mogrify -resize %s %s)\n" % (resize, filename)
2032 )
2033 return(True)
2034 if _useImageMagick('-resize %s "%s"' % (resize, filename)):
2035 sys.stderr.write(
2036 u'\n! Warning: Resizing failed command (mogrify -resize %s "%s")\n' % (resize, filename)
2037 )
2038 return(False)
2039 return True
2040 # end _resizeGraphic
2041
2042 def _downloadURL(self, url, OutputFileName):
2043 """Download the specified graphic file from a URL
2044 return False if no file was downloaded
2045 return True if a file was successfully downloaded
2046 """
2047 # Only download a file if it does not exist or the option overwrite is selected
2048 if not self.config['overwrite'] and os.path.isfile(OutputFileName):
2049 return False
2050
2051 if self.config['simulation']:
2052 sys.stdout.write(
2053 u"Simulation download of URL(%s) to File(%s)\n" % (url, OutputFileName)
2054 )
2055 return(True)
2056
2057 org_url = url
2058 tmp_URL = url.replace("http://", "")
2059 url = "http://"+urllib.quote(tmp_URL.encode("utf-8"))
2060
2061 try:
2062 dat = urllib.urlopen(url).read()
2063 except IOError, e:
2064 sys.stderr.write( u"\n! Warning: Download IOError on URL for Filename(%s)\nOrginal URL(%s)\nIOError urllib.quote URL(%s)\nError:(%s)\n" % (OutputFileName, org_url, url, e))
2065 return False
2066
2067 try:
2068 target_socket = open(OutputFileName, "wb")
2069 target_socket.write(dat)
2070 target_socket.close()
2071 except IOError, e:
2072 sys.stderr.write( u"\n! Warning: Download IOError for Filename(%s), may be the directory is invalid\nError:(%s)\n" % (OutputFileName, e))
2073 return False
2074
2075 # Verify that the downloaded file was NOT HTML instead of the intended file
2076 try:
2077 p = subprocess.Popen(u'file "%s"' % OutputFileName, shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
2078 except Exception, e:
2079 sys.stderr.write( u"\n! Warning: Download Exception for Filename(%s)\nError:(%s)\n" % (OutputFileName, e))
2080 return False
2081 except:
2082 return False
2083 data = p.stdout.readline()
2084 try:
2085 data = data.encode('utf8')
2086 except UnicodeDecodeError:
2087 data = unicode(data,'utf8')
2088 index = data.find(u'HTML document text')
2089 if index == -1:
2090 return True
2091 else:
2092 os.remove(OutputFileName) # Delete the useless HTML text
2093 if self.config['mythtv_verbose']:
2094 sys.stderr.write( u"\n! Warning: The web site may be having issues.\nURL (%s)\nReturned a file containing HTML\n(%s).\nThe bad downloaded file was removed.\n" % (url, OutputFileName))
2095 return False
2096 # end _downloadURL
2097
2098 def _setGraphicsFileNameFormat(self):
2099 """Return a file name format (e.g. seriesname - episode name.extention)
2100 return a filename format string
2101 """
2102 if self.config['g_defaultname']:
2103 return u'%(url)s.%(ext)s'
2104 cfile={}
2105 cfile['seriesid']=self.config['sid']
2106 cfile['series'] = self.sanitiseFileName(self.config['series_name'])
2107 if cfile['series'] != self.config['series_name']:
2108 self.config['g_series'] = self.config['g_series'].replace(self.config['series_name'], cfile['series'])
2109 if self.config['season_num']:
2110 cfile['seasonnumber']=int(self.config['season_num'])
2111 else:
2112 cfile['seasonnumber']=0
2113 if self.config['episode_num']:
2114 cfile['episodenumber']=int(self.config['episode_num'])
2115 else:
2116 cfile['episodenumber']=0
2117 cfile['episodename']=self.config['episode_name']
2118 cfile['seq']=u'%(seq)02d'
2119 cfile['ext']=u'%(ext)s'
2120
2121 if self.config['season_num']:
2122 return self.config['g_season'] % cfile
2123
2124 return self.config['g_series'] % cfile
2125 # end _setGraphicsFileNameFormat
2126
2127 def _downloadGraphics(self, urls, mythtv=False):
2128 """Download graphic file(s) from a URL list (string of one or more URLs separated by a CR
2129 character)
2130 return None is the string of urls has no urls
2131 return False if the any of the urls are corrupt
2132 return file name of the LAST file downloaded (special for MythTV data base updates)
2133 """
2134 global graphicsDirectories
2135
2136 if urls == None: return None
2137 if urls == '': return None
2138 tmp_list=urls.split('\n')
2139 url_list=[]
2140 for x in tmp_list:
2141 x = x.rstrip()
2142 if x != '':
2143 url_list.append(x)
2144 if not len(url_list):
2145 return None # There were no URLs in the list
2146 url_dict={}
2147 for x in url_list:
2148 try:
2149 self.config['log'].debug(u'Checking for a key in (%s)' % (x))
2150 i = x.index(':')
2151 except:
2152 sys.stderr.write(
2153 u"\n! Warning: URL list does not have a graphics type key(%s)\n" % (x)
2154 )
2155 return(False)
2156 if url_dict.has_key(x[:i]):
2157 temp_array = [x[i+1:],'']
2158 url_dict[x[:i]].append(temp_array)# Collect a list of the same graphics type of URLs
2159 else: # The first URL of a new graphics type. Also URL replacement code left in place just in case
2160 url_dict[x[:i]]=[[(x[i+1:]).replace(u"http://www.thetvdb.com",u"http://www.thetvdb.com"),'']]
2161
2162 unique_dir={u'poster': ['posterdir', True], u'banner': ['bannerdir', True], u'fanart': ['fanartdir', True], u'filename': ['episodeimagedir', True]}
2163 # If a graphics directory was not specified then default to the 'allgraphics' directory
2164 if not self.config['posterdir']: self.config['posterdir'] = self.config['allgraphicsdir']
2165 if not self.config['bannerdir']: self.config['bannerdir'] = self.config['allgraphicsdir']
2166 if not self.config['fanartdir']: self.config['fanartdir'] = self.config['allgraphicsdir']
2167 if not self.config['episodeimagedir']: self.config['episodeimagedir'] = self.config['allgraphicsdir']
2168
2169 # Check if any of the downloaded graphics will share the same directory
2170 for key in unique_dir.keys():
2171 for k in unique_dir.keys():
2172 if key != k:
2173 if self.config[unique_dir[key][0]] == self.config[unique_dir[k][0]]:
2174 unique_dir[key][1] = False
2175 break
2176
2177 dirs={u'poster': self.config['posterdir'], u'banner': self.config['bannerdir'],
2178 u'fanart': self.config['fanartdir'], u'filename': self.config['episodeimagedir']}
2179
2180 # Figure out filenaming convention
2181 file_format = self._setGraphicsFileNameFormat()
2182
2183 # Set the graphics fully qualified filenames matched to a URL
2184 for URLtype in url_dict:
2185 if mythtv:
2186 if self.absolutepath:
2187 if URLtype == 'poster':
2188 tmpgraphicdir = graphicsDirectories['coverfile']
2189 else:
2190 tmpgraphicdir = graphicsDirectories[URLtype]
2191 if not len(self.config['localpaths'][tmpgraphicdir]):
2192 return None
2193 else:
2194 directory = self.config['localpaths'][tmpgraphicdir][0]
2195 else:
2196 directory = dirs[URLtype][0]
2197 else:
2198 directory = dirs[URLtype]
2199 seq_num = 0
2200 for url in url_dict[URLtype]:
2201 (dirName, fileName) = os.path.split(url[0])
2202 (fileBaseName, fileExtension)=os.path.splitext(fileName)
2203 fileBaseName = self.sanitiseFileName(fileBaseName)
2204 # Fix file extentions in all caps or 4 character JPEG extentions
2205 fileExtension = fileExtension.lower()
2206 if fileExtension == '.jpeg':
2207 fileExtension = '.jpg'
2208 cfile={u'url': fileBaseName, u'seq': seq_num, u'ext': fileExtension[1:]}
2209 if not isValidPosixFilename(self.config['series_name']):
2210 if file_format.startswith(self.config['series_name']):
2211 file_format = file_format.replace(self.config['series_name'], self.sanitiseFileName(self.config['series_name']))
2212 cfile['series'] = self.sanitiseFileName(self.config['series_name'])
2213 cfile['seriesid'] = self.config['sid']
2214
2215 if URLtype != 'filename':
2216 if unique_dir[URLtype][1]:
2217 url_dict[URLtype][seq_num][1] = directory+'/'+file_format % cfile
2218 else:
2219 if mythtv:
2220 url_dict[URLtype][seq_num][1] = directory+'/'+file_format % cfile
2221 else:
2222 url_dict[URLtype][seq_num][1] = directory+'/'+URLtype.capitalize()+' - '+file_format % cfile
2223 else:
2224 if self.config['season_num']:
2225 cfile['seasonnumber']=int(self.config['season_num'])
2226 else:
2227 cfile['seasonnumber'] = 0
2228 if self.config['episode_num']:
2229 cfile['episodenumber']=int(self.config['episode_num'])
2230 else:
2231 cfile['episodenumber'] = 0
2232 cfile['episodename'] = self.sanitiseFileName(self.config['episode_name'])
2233 url_dict[URLtype][seq_num][1] = directory+'/'+self.config['ep_metadata'] % cfile
2234 seq_num+=1
2235
2236 # Download the graphics and resize if requested - Ignore download or resize issues!
2237 failed_download = False
2238 for URLtype in url_dict:
2239 seq_num=0
2240 for pairs in url_dict[URLtype]:
2241 if self._downloadURL(pairs[0], pairs[1]):
2242 if URLtype == u'poster' and self.config['posterresize']:
2243 self._resizeGraphic(pairs[1], self.config['posterresize'])
2244 elif URLtype == u'fanart' and self.config['fanartresize']:
2245 self._resizeGraphic(pairs[1], self.config['fanartresize'])
2246 elif not os.path.isfile(pairs[1]): # Check if the file already was downloaded
2247 failed_download = True # The download failed
2248 if self.config['mythtv_verbose']:
2249 sys.stderr.write(u'\nA graphics file failed to be downloaded. A file issue or a corrupt (HTML) file.(%s)\n' % pairs[1])
2250 seq_num+=1
2251 if self.config['maximum']: # Has the maximum number of graphics been downloaded?
2252 if seq_num == int(self.config['maximum']):
2253 break
2254 if failed_download:
2255 return None
2256 else:
2257 return pairs[1] # The name of the LAST graphics successfully downloaded
2258 # end _downloadGraphics
2259
2260 def getGraphics(self, graphics_type):
2261 """Retrieve Poster or Fan Art or Banner or Episode image graphics URL(s)
2262 return None if no graphics URLs were found
2263 return a string of URLs
2264 """
2265 banners=u'_banners'
2266 series_name=self.config['series_name']
2267 season=self.config['season_num']
2268 episode=self.config['episode_num']
2269 episode_name=self.config['episode_name']
2270 lang=self.config['local_language']
2271 graphics=[]
2272
2273 try:
2274 if self.config['sid']:
2275 URLs = self.config['tvdb_api'].ttvdb_parseBanners(self.config['sid'])
2276 else:
2277 URLs = self.config['tvdb_api'].ttvdb_parseBanners(self.config['tvdb_api']._nameToSid(series_name))
2278 except Exception, e:
2279 return None
2280
2281 if graphics_type == self.fanart_type: # Series fanart graphics
2282 if not len(URLs[u'fanart']):
2283 return None
2284 for url in URLs[u'fanart']:
2285 graphics.append(url)
2286 elif season == None and episode == None and episode_name == None:
2287 if not len(URLs[u'series']):
2288 return None
2289 if graphics_type == self.banner_type: # Series Banners
2290 for url in URLs[u'series']:
2291 graphics.append(url)
2292 else: # Series Posters
2293 for url in URLs[u'poster']:
2294 graphics.append(url)
2295 else:
2296 if not len(URLs[u'season']):
2297 return None
2298 if graphics_type == self.banner_type: # Season Banners
2299 season_banners=[]
2300 for url in URLs[u'season']:
2301 if url[u'bannertype2'] == u'seasonwide' and url[u'season'] == season:
2302 season_banners.append(url)
2303 if not len(season_banners):
2304 return None
2305 graphics = season_banners
2306 else: # Season Posters
2307 season_posters=[]
2308 for url in URLs[u'season']:
2309 if url[u'bannertype2'] == u'season' and url[u'season'] == season:
2310 season_posters.append(url)
2311 if not len(season_posters):
2312 return None
2313 graphics = season_posters
2314
2315 graphicsURLs=u''
2316 if self.config['nokeys'] and not self.config['download']:
2317 key_tag=u''
2318 else:
2319 key_tag=graphics_type+u':'
2320
2321 count = 0
2322 wasanythingadded = 0
2323 anyotherlanguagegraphics=u''
2324 englishlanguagegraphics=u''
2325 for URL in graphics:
2326 if graphics_type == 'filename':
2327 if URL[graphics_type] == None:
2328 continue
2329 if lang: # Is there a language to filter URLs on?
2330 if lang == URL['language']:
2331 if graphics_type != self.ep_image_type:
2332 graphicsURLs+=key_tag+URL['_bannerpath']+'\n'
2333 else:
2334 graphicsURLs+=key_tag+URL[graphics_type]+'\n'
2335 else: # Check for fall back graphics in case there are no selected language graphics
2336 if u'en' == URL['language']:
2337 if graphics_type != self.ep_image_type:
2338 englishlanguagegraphics+=key_tag+URL['_bannerpath']+'\n'
2339 else:
2340 englishlanguagegraphics+=key_tag+URL[graphics_type]+'\n'
2341 else:
2342 if graphics_type != self.ep_image_type:
2343 anyotherlanguagegraphics+=key_tag+URL['_bannerpath']+'\n'
2344 else:
2345 anyotherlanguagegraphics+=key_tag+URL[graphics_type]+'\n'
2346 else:
2347 if graphics_type != self.ep_image_type:
2348 graphicsURLs+=key_tag+URL['_bannerpath']+'\n'
2349 else:
2350 graphicsURLs+=key_tag+URL[graphics_type]+'\n'
2351 if wasanythingadded == len(graphicsURLs):
2352 continue
2353 wasanythingadded = len(graphicsURLs)
2354 count+=1
2355 if self.config['maximum']: # Has the maximum number of graphics been downloaded?
2356 if count == int(self.config['maximum']):
2357 break
2358
2359 if not len(graphicsURLs):
2360 if len(englishlanguagegraphics): # Fall back to English graphics
2361 graphicsURLs = englishlanguagegraphics
2362 elif len(anyotherlanguagegraphics): # Fall back-back to any available graphics
2363 graphicsURLs = anyotherlanguagegraphics
2364
2365 if self.config['debug_enabled']:
2366 print "\nGraphics:\n", graphicsURLs
2367
2368 if not len(graphicsURLs): # Are there any graphics?
2369 return None
2370
2371 if len(graphicsURLs) == 1 and graphicsURLs[0] == graphics_type+':':
2372 return None # Due to the language filter there may not be any URLs
2373
2374 return(graphicsURLs)
2375 # end get_graphics
2376
2377 def getTopRatedGraphics(self, graphics_type):
2378 """Retrieve only the top rated series Poster, Fan Art and Banner graphics URL(s)
2379 return None if no top rated graphics URLs were found
2380 return a string of top rated URLs
2381 """
2382 if graphics_type == u'filename':
2383 self.config['log'].debug(u'! There are no such thing as top rated Episode image URLs')
2384 return None
2385 toprated=None
2386 series_name=self.config['series_name']
2387 keys=self.config['nokeys']
2388 if self._searchforSeries(series_name)[graphics_type] != None:
2389 if keys and not self.config['download']:
2390 toprated=(self._searchforSeries(series_name)[graphics_type])+'\n'
2391 else:
2392 toprated=(u'%s:%s\n' % (graphics_type, self._searchforSeries(series_name)[graphics_type]))
2393 return toprated
2394 # end getTopRatedGraphics
2395
2396 def _downloadEpisodeData(self,ep_data):
2397 """Down load episode meta data and episode image graphics
2398 return True whether or not there was episode data processed
2399 """
2400 if not len(ep_data):
2401 return True # There were no episode data in the list
2402 ep_data_list=[] # An array of episode meta data
2403
2404 first_key=self.config['ep_include_data'][0]+':'
2405 key_size=len(first_key)
2406
2407 while len(ep_data): # Grab each episode's set of meta data
2408 try:
2409 self.config['log'].debug(u'Parse out the episode data from an episode meta dats string')
2410 end = ep_data[key_size:].index(first_key)
2411 ep_data_list.append(ep_data[:end+key_size])
2412 ep_data=ep_data[end+key_size:]
2413 except:
2414 ep_data_list.append(ep_data)
2415 break
2416
2417 if not self.config['metadatadir']:
2418 self.config['metadatadir'] = os.getcwd()
2419
2420 # Process each episode's meta data
2421 for episode in ep_data_list:
2422 tmp_data = episode.split('\n')
2423 for i in range(len(tmp_data)):
2424 tmp_data[i] = tmp_data[i].rstrip()# Remove \n characters from the end of each record
2425 tmp_dict ={}
2426 for data in tmp_data:
2427 try:
2428 self.config['log'].debug(u'Checking for key in episode meta data')
2429 tmp_dict[data[:data.index(':')]] = data[data.index(':')+1:]
2430 except ValueError:
2431 continue
2432 tmp_dict['ext']='meta'
2433 tmp_dict['seq']=0
2434 for key in ['seasonnumber', 'episodenumber']:
2435 if tmp_dict.has_key(key):
2436 tmp_dict[key] = int(tmp_dict[key])
2437 if not tmp_dict.has_key(u'episodename'):
2438 tmp_dict[u'episodename'] = u''
2439 filename="%s/%s" % (self.config['metadatadir'],self.config['ep_metadata'] % tmp_dict)
2440 image_filename = None
2441 if self.config['get_ep_image'] and tmp_dict.has_key('filename'):
2442 url= tmp_dict['filename']
2443 if url != 'None':
2444 if not self.config['episodeimagedir']:
2445 self.config['episodeimagedir'] = self.config['allgraphicsdir']
2446 (dirName, fileName) = os.path.split(url)
2447 (fileBaseName, fileExtension)=os.path.splitext(fileName)
2448 tmp_dict[u'ext']=fileExtension[1:]
2449 image_filename = "%s/%s" % (self.config['episodeimagedir'], self.config['ep_metadata'] % tmp_dict)
2450 # Only download a file if it does not exist or the option overwrite is selected
2451 # or the option update is selected and the local meta data file is
2452 # older than the episode data on thetvdb.com wiki
2453 outofdate = False
2454 if self.config['update'] and tmp_dict.has_key('lastupdated') and os.path.isfile(filename):
2455 if int(tmp_dict['lastupdated']) > int(os.path.getmtime(filename)):
2456 outofdate= True
2457
2458 if not self.config['overwrite'] and not outofdate:
2459 if self.config['get_ep_meta'] and self.config['get_ep_image']:
2460 if image_filename:
2461 if os.path.isfile(filename) and os.path.isfile(image_filename):
2462 continue
2463 else:
2464 if os.path.isfile(filename):
2465 continue
2466 elif self.config['get_ep_meta']:
2467 if os.path.isfile(filename):
2468 continue
2469 elif self.config['get_ep_image'] and tmp_dict.has_key('filename'):
2470 url= tmp_dict['filename']
2471 if url != 'None':
2472 if os.path.isfile(image_filename):
2473 continue
2474 else:
2475 continue
2476 else:
2477 continue
2478
2479 if self.config['simulation']:
2480 if self.config['get_ep_image'] and tmp_dict.has_key('filename'):
2481 self.config['log'].debug(u'Simulate downloading an episode image')
2482 url= tmp_dict['filename']
2483 if url != 'None':
2484 sys.stdout.write(u"Simulation create episode image file(%s)\n" % image_filename)
2485 if self.config['get_ep_meta']:
2486 sys.stdout.write(
2487 u"Simulation create meta data file(%s)\n" % filename
2488 )
2489 continue
2490
2491 if self.config['get_ep_image'] and tmp_dict.has_key('filename'):
2492 if tmp_dict['filename'] != 'None':
2493 self._downloadGraphics('filename:'+tmp_dict['filename'])
2494
2495 # Write out an episode meta data file
2496 if self.config['get_ep_meta']:
2497 fHandle = codecs.open(filename, 'w', 'utf8')
2498 fHandle.write(episode)
2499 fHandle.close()
2500
2501 return True
2502 # end _downloadEpisodeData
2503
2504 def _changeToCommas(self,meta_data):
2505 """Remove '|' and replace with commas
2506 return the modified text
2507 """
2508 if not meta_data: return meta_data
2509 meta_data = (u'|'.join([d for d in meta_data.split('| ') if d]))
2510 return (u', '.join([d for d in meta_data.split(u'|') if d]))
2511 # end _changeToCommas
2512
2513 def _changeAmp(self, text):
2514 """Change &amp; values to ASCII equivalents
2515 return the modified text
2516 """
2517 if not text: return text
2518 text = text.replace("&quot;", u"'").replace("\r\n", u" ")
2519 text = text.replace(r"\'", u"'")
2520 return text
2521 # end _changeAmp
2522
2523 def getSeriesEpisodeData(self):
2524 """Get Series Episode meta data. This can be one specific episode or all of a seasons episodes
2525 or all episodes for an entire series.
2526 return an empy sting of no episode meta data was found
2527 reurn a string containing key value pairs of episode meta data
2528 """
2529 sid=self.config['sid']
2530 series_name=self.config['series_name']
2531 season_num=self.config['season_num']
2532 episode_num=self.config['episode_num']
2533 episode_name=self.config['episode_name']
2534
2535 # Get Cast members
2536 tmp_cast={}
2537 cast_members=''
2538 try:
2539 tmp_cast = self._searchforSeries(series_name)[u'_actors']
2540 except:
2541 cast_members=''
2542 if len(tmp_cast):
2543 cast_members=''
2544 for cast in tmp_cast:
2545 cast_members+=(cast['name']+u', ').encode('utf8')
2546 if cast_members != '':
2547 try:
2548 cast_members = cast_members[:-2].encode('utf8')
2549 except UnicodeDecodeError:
2550 cast_members = unicode(cast_members[:-2],'utf8')
2551 cast_members = self._changeAmp(cast_members)
2552 cast_members = self._changeToCommas(cast_members)
2553 cast_members=cast_members.replace('\n',' ')
2554
2555 # Get genre(s)
2556 genres=''
2557 try:
2558 genres_string = self._searchforSeries(series_name)[u'genre'].encode('utf8')
2559 except:
2560 genres_string=''
2561 if genres_string != None and genres_string != '':
2562 genres = self._changeAmp(genres_string)
2563 genres = self._changeToCommas(genres)
2564
2565 seasons=self._searchforSeries(series_name).keys() # Get the seasons for this series
2566 episodes_metadata=u''
2567 for season in seasons:
2568 if season_num: # If a season was specified skip other seasons
2569 if season != int(season_num):
2570 continue
2571 episodes=self._searchforSeries(series_name)[season].keys()# Get the episodes for this season
2572 for episode in episodes: # If an episode was specified skip other episodes
2573 if episode_num:
2574 if episode != int(episode_num):
2575 continue
2576 ep_data={}
2577 if sid: # Ouput the full series name
2578 try:
2579 ep_data["series"]=self._searchforSeries(sid)[u'seriesname'].encode('utf8')
2580 except AttributeError:
2581 return u''
2582 else:
2583 try:
2584 ep_data["series"]=self._searchforSeries(series_name)[u'seriesname'].encode('utf8')
2585 except AttributeError:
2586 return u''
2587 available_keys=self._searchforSeries(series_name)[season][episode].keys()
2588 tmp=u''
2589 ep_data[u'gueststars']=''
2590 for key in available_keys:
2591 if self._searchforSeries(series_name)[season][episode][key] == None:
2592 continue
2593 # Massage meta data
2594 text = self._searchforSeries(series_name)[season][episode][key]
2595 text = self._changeAmp(text)
2596 text = self._changeToCommas(text)
2597 ep_data[key.lower()]=text.replace('\n',' ')
2598 for key in self.config['ep_include_data']: # Select and sort the required meta data
2599 if ep_data.has_key(key):
2600 if key == u'gueststars':
2601 if ep_data[key] == '':
2602 tmp+=u'Cast:%s\n' % cast_members
2603 else:
2604 if (len(ep_data[key]) > 128) and not ep_data[key].count(','):
2605 tmp+=u'Cast:%s\n' % cast_members
2606 else:
2607 tmp+=u'Cast:%s, %s\n' % (cast_members, ep_data[key])
2608 continue
2609 try:
2610 tmp+=u'%s:%s\n' % (key, ep_data[key])
2611 except UnicodeDecodeError:
2612 tmp+=u'%s:%s\n' % (key, unicode(ep_data[key], "utf8"))
2613 tmp+=u'Runtime:%s\n' % self._searchforSeries(series_name)[u'runtime']
2614 if genres != '':
2615 tmp+=u'Genres:%s\n' % genres
2616 if len(tmp) > 0:
2617 episodes_metadata+=tmp
2618 return episodes_metadata
2619 # end Getseries_episode_data
2620
2621 def returnFilename(self):
2622 """Return a single file name (excluding file extension and directory), limited by the current
2623 variables (sid, season name, season number ... etc). Typically used when writing a meta file
2624 or naming/renaming a video file after a TV show recording.
2625 return False and out put an error if there not either a series id (SID) or series name
2626 return False and out put an error if there proper episode information (numbers or name)
2627 return False if the option (-MGF) used and there is not exact TV series name match
2628 return a specific episode filename
2629 """
2630 sid=self.config['sid']
2631 series_name=self.config['series_name']
2632 season_num=self.config['season_num']
2633 episode_num=self.config['episode_num']
2634 episode_name=self.config['episode_name']
2635
2636 if not sid and not series_name:
2637 sys.stderr.write(
2638 u"\n! Warning: There must be at least series name or SID to request a filename\n"
2639 )
2640 return False
2641
2642 if season_num and episode_num:
2643 pass
2644 elif not episode_name:
2645 sys.stderr.write(
2646 u'\n! Error: There must be at least "season and episode numbers" or "episode name" to request a filename\n'
2647 )
2648 sys.exit(1)
2649
2650 # Special logic must be used if the (-MG) guessing option has been requested
2651 if not self.config['sid'] and self.config['mythtv_guess']:
2652 try:
2653 allmatchingseries = self.config['tvdb_api']._getSeries(self.config['series_name'])
2654 except Exception, e:
2655 sys.stderr.write(u"\nErrors while trying to contact thetvbd.com for Series (%s)\ntherefore a file rename is not possible. error(%s)\n\n" % (self.config['series_name'], e))
2656 return False
2657 if filter(is_not_punct_char, allmatchingseries['name'].lower()) == filter(is_not_punct_char, self.config['series_name'].lower()):
2658 self.config['sid'] = allmatchingseries['sid']
2659 self.config['series_name'] = allmatchingseries['name']
2660 else:
2661 sys.stderr.write(u"\nGuessing could not find an exact match on tvdb for Series (%s)\ntherefore a file rename is not possible.\n\n" % self.config['series_name'])
2662 return False
2663
2664 episode = self.verifySeriesExists()
2665
2666 if not episode: # Make sure an episode was found
2667 sys.stderr.write(
2668 u'\n! Error: The episode was not found for series(%s), Episode name(%s)\n' % (series_name, episode_name)
2669 )
2670 sys.exit(1)
2671
2672 sid=self.config['sid']
2673
2674 if UI_selectedtitle and (self.config['mythtv_inetref'] or self.config['mythtv_ref_num']):
2675 self.config['series_name'] = UI_selectedtitle
2676
2677 series_name=self.config['series_name']
2678 season_num=self.config['season_num']
2679 episode_num=self.config['episode_num']
2680 episode_name=self.config['episode_name']
2681
2682 tmp_dict ={'series': series_name, 'seasonnumber': season_num, 'episodenumber': episode_num, 'episodename': episode_name, 'sid': sid }
2683
2684 tmp_dict['ext']=''
2685 for key in ['seasonnumber', 'episodenumber']:
2686 if tmp_dict.has_key(key):
2687 tmp_dict[key] = int(tmp_dict[key])
2688
2689 return self.sanitiseFileName(u"%s" % (self.config['ep_metadata'] % tmp_dict)[:-1])
2690 # end returnFilename
2691
2692 def processTVdatabaseRequests(self):
2693 """Process the data/download requests as indicated by the variables
2694 return None if the series/season/episode does not exist
2695 return None if there is no data to process for the request actions
2696 return a string for display or further processing that satisfies the reqested actions
2697 """
2698 if self.verifySeriesExists():# Getting a filename is a single event nothing else is returned
2699 if self.config['ret_filename']:
2700 return self.returnFilename()
2701 else:
2702 return None
2703
2704 types={'get_fanart': self.fanart_type, 'get_poster': self.poster_type, 'get_banner': self.banner_type}
2705 if self.config['toprated']:
2706 typegetGraphics=self.getTopRatedGraphics
2707 else:
2708 typegetGraphics=self.getGraphics
2709 results=u''
2710 if self.verifySeriesExists():
2711 if self.config['download']: # Deal only with graphics display or downloads
2712 for key in types.keys():
2713 if key == 'get_ep_image': # Ep image downloads processed below
2714 continue
2715 if self.config[key]:
2716 if self._downloadGraphics(typegetGraphics(types[key])):
2717 sys.stdout.write(
2718 u"%s downloading successfully processed\n" % key.title()
2719 )
2720 else:
2721 url_string=u''
2722 for key in types.keys():
2723 if self.config[key]:
2724 string=typegetGraphics(types[key])
2725 if string != None:
2726 url_string+=string
2727 if url_string != '':
2728 results+=url_string # Add graphic URLs to returned results
2729
2730 # Should episode meta data or episode image be processed?
2731 if self.config['get_ep_meta'] or self.config['get_ep_image']:
2732 if self.config['download']: # Deal only with episode data display or download
2733 if self._downloadEpisodeData(self.getSeriesEpisodeData()):
2734 sys.stdout.write(
2735 u"Episode meta data and/or images downloads successfully processed\n"
2736 )
2737 else:
2738 eps_string = self.getSeriesEpisodeData()
2739 if eps_string != '':
2740 results+=eps_string # Add episode meta data to returned results
2741 else:
2742 return None
2743
2744 if results != u'':
2745 if results[len(results)-1] == '\n':
2746 return results[:len(results)-1]
2747 else:
2748 return results
2749 else:
2750 return None
2751 # end processTVdatabaseRequests
2752
2753 def __repr__(self): # Just a place holder
2754 return self.config
2755 # end __repr__
2756
2757 # end Tvdatabase
2758
2759
2760 class VideoFiles(Tvdatabase):
2761 """Process all video file and/or directories containing video files. These TV Series video
2762 files must be named so that a "series name or sid" and/or "season and episode number"
2763 can be extracted from the video file name. It is best to have renamed the TV series video files with
2764 tvnamer before using these files with jamu. Any video file without season and episode numbers is
2765 assumed to be a movie. Files that do not match the previously described criterion will be skipped.
2766 tvnamer can be found at:
2767 http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search
2768 """
2769 def __init__(self, configuration):
2770 """Retrieve the configuration options
2771 """
2772 super(VideoFiles, self).__init__(configuration)
2773 # end __init__
2774
2775 image_extensions = ["png", "jpg", "bmp"]
2776
2777 def _findFiles(self, args, recursive = False, verbose = False):
2778 """
2779 Takes a file name or folder path and grabs files inside them. Does not recurse
2780 more than one level (if a folder is supplied, it will list files within),
2781 unless recurse is True, in which case it will recursively find all files.
2782 return an array of file names
2783 """
2784 allfiles = []
2785
2786 for cfile in args: # Directories must exist and be both readable and writable
2787 if os.path.isdir(cfile) and not os.access(cfile, os.F_OK | os.R_OK):
2788 sys.stderr.write(u"\n! Error: Video directory (%s) does not exist or the permissions are not at least readable. Skipping this directory.\n" % (cfile))
2789 continue
2790 ignore = False
2791 if os.path.isdir(cfile):
2792 # ramsi allow regex in ignore-directory
2793 for directory in self.config['ignore-directory']: # ignore directory list
2794 #if not cfile.startswith(directory):
2795 if re.search(directory,cfile) is None:
2796 print "yes"
2797 continue
2798 ignore = True
2799 if ignore: # Skip this directory
2800 continue
2801 if os.path.isdir(cfile):
2802 index = cfile.find(u'VIDEO_TS')
2803 if index != -1:
2804 sys.stderr.write(u"\n! Warning: Jamu does not process multi-part video files, video directory (%s).\nSkipping this directory. Use MythVideo to retrieve meta data for these video files.\n" % (cfile))
2805 continue
2806 try:
2807 cfile = unicode(cfile, u'utf8')
2808 except (UnicodeEncodeError, TypeError):
2809 pass
2810 for sf in os.listdir(cfile):
2811 try:
2812 newpath = os.path.join(cfile, sf)
2813 except:
2814 sys.stderr.write(u"\n! Error: This video file cannot be processed skipping:\n")
2815 sys.stderr.write(sf)
2816 sys.stderr.write(u"\nIt may be advisable to rename this file and try again.\n\n")
2817 continue
2818 if os.path.isfile(newpath):
2819 allfiles.append(newpath)
2820 else:
2821 if recursive:
2822 allfiles.extend(
2823 self._findFiles([newpath], recursive = recursive, verbose = verbose)
2824 )
2825 #end if recursive
2826 #end if isfile
2827 #end for sf
2828 elif self.config[u'file_move_flag'] and not os.access(cfile, os.F_OK | os.R_OK | os.W_OK):
2829 sys.stderr.write(u"\n! Error: The Video file (%s) to be moved must have the read and write permissions. Skipping this video file.\n" % (cfile))
2830 elif os.path.isfile(cfile) and os.access(cfile, os.F_OK | os.R_OK):
2831 allfiles.append(cfile) # Files must exist and be at least readable
2832 #end if isdir
2833 #end for cfile
2834 return allfiles
2835 #end findFiles
2836
2837
2838 def _processNames(self, names, verbose=False, movies=False):
2839 """
2840 Takes list of names, runs them though the self.config['name_parse'] regex parsing strings
2841 to extract series name, season and episode numbers. Non-video files are skipped.
2842 return an array of dictionaries containing series name, season and episode numbers, file path and full filename and file extention.
2843 """
2844 allEps = []
2845 for f in names:
2846 filepath, filename = os.path.split( f )
2847 filename, ext = os.path.splitext( filename )
2848
2849 # Remove leading . from extension
2850 ext = ext.replace(u".", u"", 1)
2851 self.config['log'].debug(u'Checking for a valid video filename extension')
2852 if not ext.lower() in self.config[u'video_file_exts']:
2853 for key in self.image_extensions:
2854 if key == ext:
2855 break
2856 else:
2857 sys.stderr.write(u"\n! Warning: Skipping non-video file name: (%s)\n" % (f))
2858 continue
2859
2860 match = None
2861 for r in self.config['name_parse']:
2862 match = r.match(filename)
2863 if match: break
2864 # If the filename does not match the default regular
2865 # expressions, try to match the file path + filename with the
2866 # extended fullpath regular expression so we can extract the
2867 # needed information out of the pathname
2868 if not match:
2869 for r in self.config['fullname_parse']:
2870 match = r.match(os.path.join(filepath, filename))
2871 if match: break
2872
2873 categories=''
2874 if match:
2875 self.config['log'].debug(u'matched reg:%s'%match.re.pattern)
2876 seriesname, seasno, epno = match.groups()
2877
2878 #remove ._- characters from name (- removed only if next to end of line)
2879 seriesname = re.sub("[\._]|\-(?=$)", " ", seriesname).strip()
2880 # ramsi remove [en] tags
2881 seriesname = re.sub("(?:\[.*\])+", " ", seriesname).strip()
2882
2883 seasno, epno = int(seasno), int(epno)
2884
2885 if self.config['series_name_override']:
2886 if self.config['series_name_override'].has_key(seriesname.lower()):
2887 if len((self.config['series_name_override'][seriesname.lower()]).strip()) == 7:
2888 categories+=u', Movie'
2889 movie = filename
2890 if movie.endswith(self.config['hd_dvd']):
2891 movie = movie.replace(self.config['hd_dvd'], '')
2892 categories+=u', DVD'
2893 categories+=u', HD'
2894 else:
2895 if movie.endswith(self.config['dvd']):
2896 movie = movie.replace(self.config['dvd'], '')
2897 categories+=u', DVD'
2898 movie = re.sub("[\._]|\-(?=$)", " ", movie).strip()
2899 # ramsi remove [en] tags
2900 movie = re.sub("(?:\[.*\])+", " ", movie).strip()
2901 try:
2902 allEps.append({ 'file_seriesname':movie,
2903 'seasno':0,
2904 'epno':0,
2905 'filepath':filepath,
2906 'filename':filename,
2907 'ext':ext,
2908 'categories': categories
2909 })
2910 except UnicodeDecodeError:
2911 allEps.append({ 'file_seriesname':unicode(movie,'utf8'),
2912 'seasno':0,
2913 'epno':0,
2914 'filepath':unicode(filepath,'utf8'),
2915 'filename':unicode(filename,'utf8'),
2916 'ext':unicode(ext,'utf8'),
2917 'categories': categories
2918 })
2919
2920 categories+=u', TV Series'
2921 try:
2922 allEps.append({ 'file_seriesname':seriesname,
2923 'seasno':seasno,
2924 'epno':epno,
2925 'filepath':filepath,
2926 'filename':filename,
2927 'ext':ext,
2928 'categories': categories
2929 })
2930 except UnicodeDecodeError:
2931 allEps.append({ 'file_seriesname':unicode(seriesname,'utf8'),
2932 'seasno':seasno,
2933 'epno':epno,
2934 'filepath':unicode(filepath,'utf8'),
2935 'filename':unicode(filename,'utf8'),
2936 'ext':unicode(ext,'utf8'),
2937 'categories': categories
2938 })
2939 else:
2940 if movies: # Account for " - On DVD" and " HD - On DVD" extra text on file names
2941 categories+=u', Movie'
2942 movie = filename
2943
2944 if movie.endswith(self.config['hd_dvd']):
2945 movie = movie.replace(self.config['hd_dvd'], '')
2946 categories+=u', DVD'
2947 categories+=u', HD'
2948 else:
2949 if movie.endswith(self.config['dvd']):
2950 movie = movie.replace(self.config['dvd'], '')
2951 categories+=u', DVD'
2952 movie = re.sub("[\._]|\-(?=$)", " ", movie).strip()
2953 # ramsi remove [en] tags
2954 movie = re.sub("(?:\[.*\])+", " ", movie).strip()
2955 try:
2956 allEps.append({ 'file_seriesname':movie,
2957 'seasno':0,
2958 'epno':0,
2959 'filepath':filepath,
2960 'filename':filename,
2961 'ext':ext,
2962 'categories': categories
2963 })
2964 except UnicodeDecodeError:
2965 allEps.append({ 'file_seriesname':unicode(movie,'utf8'),
2966 'seasno':0,
2967 'epno':0,
2968 'filepath':unicode(filepath,'utf8'),
2969 'filename':unicode(filename,'utf8'),
2970 'ext':unicode(ext,'utf8'),
2971 'categories': categories
2972 })
2973 else:
2974 sys.stderr.write(u"\n! Warning: Skipping invalid name: %s\n" % (f))
2975 #end for r
2976 #end for f
2977
2978 return allEps
2979 #end processNames
2980
2981
2982 def processFileOrDirectory(self):
2983 '''This routine is NOT used for MythTV meta data processing.
2984 If directory path has been specified then create a list of files that qualify as video
2985 files / including recursed directories.
2986 Then parse the list of file names to determine (series, season number, ep number and ep name).
2987 Skip any video file that cannot be parsed for sufficient info.
2988 Loop through the list:
2989 > Check if the series, season, ... exists, skip with debug message if none found
2990 > Set variable with proper info: sid, series, season and episode numbers
2991 > Process the file's information per the variable to get graphics and or meta data
2992 return False and an error message and exist the script if there are no video files to process
2993 return None when all processing was complete
2994 return a string of file names if the "Filename" process option was True
2995 '''
2996 filenames=''
2997 allFiles = self._findFiles(self.config['video_dir'], self.config['recursive'] , verbose = self.config['debug_enabled'])
2998 validFiles = self._processNames(allFiles, verbose = self.config['debug_enabled'])
2999
3000 if len(validFiles) == 0:
3001 sys.stderr.write(u"\n! Error: No valid video files found\n")
3002 sys.exit(1)
3003
3004 path_flag = self.config['metadatadir']
3005 for cfile in validFiles:
3006 sys.stdout.write(u"# Processing %(file_seriesname)s (season: %(seasno)d, episode %(epno)d)\n" % (cfile))
3007 self.config['sid']=None
3008 self.config['episode_name'] = None
3009 self.config['series_name']=cfile['file_seriesname']
3010 self.config['season_num']=u"%d" % cfile['seasno']
3011 self.config['episode_num']=u"%d" % cfile['epno']
3012 if not path_flag: # If no metaddata directory specified then default to the video file dir
3013 self.config['metadatadir'] = cfile['filepath']
3014 if self.verifySeriesExists():
3015 self.config['log'].debug(u"Found series(%s) season(%s) episode(%s)" % (self.config['series_name'], self.config['season_num'], self.config['episode_num']))
3016 if self.config['ret_filename']:
3017 returned = self.processTVdatabaseRequests()
3018 if returned != None and returned != False:
3019 filenames+=returned+'\n'
3020 else:
3021 self.processTVdatabaseRequests()
3022 else:
3023 sys.stderr.write(u"\n! Warning: Did not find series(%s) season(%s) episode(%s)\n" % (self.config['series_name'], self.config['season_num'], self.config['episode_num']))
3024 self.config['log'].debug("# Done")
3025 if len(filenames) == 0:
3026 return None
3027 else:
3028 return filenames[:-1] # drop the last '\n'
3029 # end processFileOrDirectory
3030
3031 def __repr__(self): # Just a place holder
3032 return self.config
3033 # end __repr__
3034
3035 # end VideoFiles
3036
3037
3038 class MythTvMetaData(VideoFiles):
3039 """Process all mythvideo video files, update the video files associated MythTV meta data.
3040 Download graphics for those video files from either thetvdb.com or themovie.com. Video file names
3041 for TV episodes must series name, season and episode numbers. The video file's movie name must be
3042 an exact match with a movie title in themoviedb.com or the MythTV database must have an entry for
3043 the video file with a TMDB or an IMDB number (db field 'intref').
3044 """
3045 def __init__(self, configuration):
3046 """Retrieve the configuration options
3047 """
3048 super(MythTvMetaData, self).__init__(configuration)
3049 # end __init__
3050
3051 # Local variables
3052 # A dictionary of meta data keys and initialized values
3053 global graphicsDirectories
3054 movie_file_format=u"%s/%s.%s"
3055 graphic_suffix = {u'coverfile': u'_coverart', u'fanart': u'_fanart', u'banner': u'_banner'}
3056 graphic_name_suffix = u"%s/%s%s.%s"
3057 graphic_name_season_suffix = u"%s/%s Season %d%s.%s"
3058
3059
3060 def _getSubtitle(self, cfile):
3061 '''Get the MythTV subtitle (episode name)
3062 return None
3063 return episode name string
3064 '''
3065 self.config['sid']=None
3066 self.config['episode_name'] = None
3067 self.config['series_name']=cfile['file_seriesname']
3068 self.config['season_num']=u"%d" % cfile['seasno']
3069 self.config['episode_num']=u"%d" % cfile['epno']
3070 self.verifySeriesExists()
3071 return self.config['episode_name']
3072 # end _getSubtitle
3073
3074
3075 def hashFile(self, name):
3076 '''Create metadata hash values for mythvideo files
3077 return a hash value
3078 return u'' if the was an error with the video file or the video file length was zero bytes
3079 '''
3080 filename = self.rtnRelativePath(name, u'mythvideo')
3081 # Use the MythVideo hashing protocol when the video is in a storage groups
3082 if filename[0] != u'/':
3083 hash_value = mythbeconn.getHash(filename, u'Videos')
3084 if hash_value == u'NULL':
3085 return u''
3086 else:
3087 return hash_value
3088
3089 # Use a local hashing routine when video is not in a Videos storage group
3090 # For original code: http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes#Python
3091 try:
3092 longlongformat = 'q' # long long
3093 bytesize = struct.calcsize(longlongformat)
3094 f = open(name, "rb")
3095 filesize = os.path.getsize(name)
3096 hash = filesize
3097 if filesize < 65536 * 2: # Video file is too small
3098 return u''
3099 for x in range(65536/bytesize):
3100 buffer = f.read(bytesize)
3101 (l_value,)= struct.unpack(longlongformat, buffer)
3102 hash += l_value
3103 hash = hash & 0xFFFFFFFFFFFFFFFF #to remain as 64bit number
3104 f.seek(max(0,filesize-65536),0)
3105 for x in range(65536/bytesize):
3106 buffer = f.read(bytesize)
3107 (l_value,)= struct.unpack(longlongformat, buffer)
3108 hash += l_value
3109 hash = hash & 0xFFFFFFFFFFFFFFFF
3110 f.close()
3111 returnedhash = "%016x" % hash
3112 return returnedhash
3113
3114 except(IOError): # Accessing to this video file caused and error
3115 return u''
3116 # end hashFile()
3117
3118 def rtnRelativePath(self, abpath, filetype):
3119 '''Check if there is a Storage Group for the file type (video, coverfile, banner, fanart, screenshot)
3120 and form an apprioriate relative path and file name.
3121 return a relative path and file name
3122 return an absolute path and file name if there is no storage group for the file type
3123 '''
3124 if abpath == None:
3125 return abpath
3126
3127 # There is a chance that this is already a relative path or there is no Storage group for file type
3128 if not len(storagegroups):
3129 return abpath
3130 if not storagegroups.has_key(filetype) or abpath[0] != '/':
3131 return abpath
3132
3133 # The file must already be in one of the directories specified by the file type's storage group
3134 for directory in storagegroups[filetype]:
3135 if abpath.startswith(directory):
3136 return abpath[len(directory)+1:]
3137 else:
3138 return abpath
3139 # end rtnRelativePath
3140
3141 def rtnAbsolutePath(self, relpath, filetype):
3142 '''Check if there is a Storage Group for the file type (mythvideo, coverfile, banner, fanart,
3143 screenshot) and form an appropriate absolute path and file name.
3144 return an absolute path and file name
3145 return the relpath sting if the file does not actually exist in the absolute path location
3146 '''
3147 if relpath == None or relpath == u'':
3148 return relpath
3149
3150 # There is a chance that this is already an absolute path
3151 if relpath[0] == u'/':
3152 return relpath
3153
3154 if self.absolutepath:
3155 if not len(self.config['localpaths'][filetype]):
3156 return relpath
3157 directories = self.config['localpaths'][filetype]
3158 else:
3159 directories = self.config[filetype]
3160
3161 for directory in directories:
3162 abpath = u"%s/%s" % (directory, relpath)
3163 if os.path.isfile(abpath): # The file must actually exist locally
3164 return abpath
3165 else:
3166 return relpath # The relative path does not exist at all the metadata entry is useless
3167 # end rtnAbsolutePath
3168
3169
3170 def removeCommonWords(self, title):
3171 '''Remove common words from a title
3172 return title striped of common words
3173 '''
3174 if not title:
3175 return u' '
3176 wordList = [u'the ', u'a ', u' '] # common word list. Leave double space as the last value.
3177 title = title.lower()
3178 for word in wordList:
3179 title = title.replace(word, u'')
3180 if not title:
3181 return u' '
3182 return filter(is_not_punct_char, title.strip())
3183 # end removeCommonWords()
3184
3185
3186 def _getTmdbIMDB(self, title, watched=False, IMDB=False, rtnyear=False):
3187 '''Find and exact match of the movie name with what's on themoviedb.com
3188 If IMDB is True return an imdb#
3189 If rtnyear is True return IMDB# and the movie year in a dictionary
3190 return False (no matching movie found)
3191 return imdb# and/or tmdb#
3192 '''
3193 global video_type, UI_title
3194 UI_title = title.replace(self.config[u'hd_dvd'], u'')
3195 UI_title = UI_title.replace(self.config[u'dvd'], u'')
3196
3197 if UI_title[-1:] == ')': # Get rid of the (XXXX) year from the movie title
3198 tmp_title = UI_title[:-7].lower()
3199 else:
3200 tmp_title = UI_title.lower()
3201
3202 if self.config['series_name_override']:
3203 if self.config['series_name_override'].has_key(tmp_title):
3204 return (self.config['series_name_override'][tmp_title]).strip()
3205
3206 TMDB_movies=[]
3207 IMDB_movies=[]
3208 user_tmdb = False
3209
3210 while True:
3211 try:
3212 if IMDB:
3213 results = [self.config['tmdb_api'].searchIMDB(IMDB)]
3214 elif user_tmdb:
3215 results = self.config['tmdb_api'].searchTMDB(user_tmdb)
3216 if rtnyear:
3217 if results.has_key('releasedate'):
3218 return {'name': "%s (%s)" % (results['title'], results['releasedate'][:4]), u'sid': results[u'inetref']}
3219 else:
3220 return {'name': "%s" % (results['title'], ), u'sid': results[u'inetref']}
3221 else:
3222 return results['inetref']
3223 else:
3224 results = self.config['tmdb_api'].searchTitle(tmp_title)
3225 except TmdbMovieOrPersonNotFound, e:
3226 results = [{}]
3227 except Exception, errormsg:
3228 self._displayMessage(u"themoviedb.com error for Movie(%s) invalid data error (%s)" % (title, errormsg))
3229 return False
3230 except:
3231 self._displayMessage(u"themoviedb.com error for Movie(%s)" % title)
3232 return False
3233
3234 # Check if user's interactive response (Skip, selection, input #)
3235 if len(results[0]) and self.config['interactive']:
3236 if results[0].has_key('userResponse'):
3237 # Check if the user selected a specific movie from the list
3238 if results[0]['userResponse'] == 'User selected':
3239 if rtnyear:
3240 if results[0].has_key('released'):
3241 data = {'name': "%s (%s)" % (results[0]['name'], results[0]['released'][:4]), u'sid': results[0][u'id']}
3242 else:
3243 data = {'name': "%s" % (results[0]['name'], ), u'sid': results[0][u'id']}
3244 return data
3245 else:
3246 return results[0]['id']
3247 # Check if the user has entered a TMDB number themselves
3248 if results[0]['userResponse'] == 'User input':
3249 user_tmdb = results[0]['id']
3250 continue
3251 # Check if the user wants this video to be ignored by Jamu from now on
3252 if results[0]['id'] == '99999999':
3253 if rtnyear:
3254 return False
3255 else:
3256 return results[0]['id']
3257 break
3258
3259 if IMDB: # This is required to allow graphic file searching both by a TMDB and IMDB numbers
3260 if len(results[0]):
3261 if results[0].has_key('imdb_id'):
3262 return results[0]['imdb_id'][2:]
3263 else:
3264 return False
3265 else:
3266 return False
3267
3268 if UI_title[-1:] == ')':
3269 name = UI_title[:-7].lower() # Just the movie title
3270 year = UI_title[-5:-1] # The movie release year
3271 else:
3272 name = tmp_title.lower()
3273 year = ''
3274 name = name.strip().replace(' ', ' ')
3275
3276 if len(results[0]):
3277 for movie in results:
3278 if self.removeCommonWords(movie['name']) == self.removeCommonWords(name):
3279 if not year:
3280 if movie.has_key('released'):
3281 TMDB_movies.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']})
3282 else:
3283 TMDB_movies.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']})
3284 continue
3285 if movie.has_key(u'released'):
3286 if movie['released'][:4] == year:
3287 if rtnyear:
3288 return {'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']}
3289 else:
3290 return movie[u'id']
3291 TMDB_movies.append({'name': "%s (%s)" % (movie['name'], movie['released'][:4]), u'sid': movie[u'id']})
3292 continue
3293 else:
3294 TMDB_movies.append({'name': "%s" % (movie['name'], ), u'sid': movie[u'id']})
3295 continue
3296 elif movie.has_key('alternative_name'):
3297 if self.removeCommonWords(movie['alternative_name']) == self.removeCommonWords(name):
3298 if not year:
3299 if movie.has_key('released'):
3300 TMDB_movies.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']})
3301 else:
3302 TMDB_movies.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']})
3303 continue
3304 if movie.has_key(u'released'):
3305 if movie['released'][:4] == year:
3306 if rtnyear:
3307 return {'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']}
3308 else:
3309 return movie['id']
3310 TMDB_movies.append({'name': "%s (%s)" % (movie['alternative_name'], movie['released'][:4]), u'sid': movie[u'id']})
3311 continue
3312 else:
3313 TMDB_movies.append({'name': "%s" % (movie['alternative_name'], ), u'sid': movie[u'id']})
3314 continue
3315
3316 # When there is only one match but NO year to confirm then it is OK to assume an exact match
3317 if len(TMDB_movies) == 1 and year == '':
3318 if rtnyear:
3319 return TMDB_movies[0]
3320 else:
3321 return TMDB_movies[0][u'sid']
3322
3323 if imdb_lib: # Can a imdb.com search be done?
3324 imdb_access = imdb.IMDb()
3325 movies_found = []
3326 try:
3327 movies_found = imdb_access.search_movie(tmp_title.encode("ascii", 'ignore'))
3328 except Exception:
3329 return False
3330 if not len(movies_found):
3331 return False
3332 tmp_movies={}
3333 for movie in movies_found: # Get rid of duplicates
3334 try: # Protect against bad data from IMDBpy
3335 if movie.has_key('year'):
3336 temp = {imdb_access.get_imdbID(movie): u"%s (%s)" % (movie['title'], movie['year'])}
3337 else:
3338 temp = {imdb_access.get_imdbID(movie): movie['title']}
3339 except Exception:
3340 return False
3341 if tmp_movies.has_key(temp.keys()[0]):
3342 continue
3343 tmp_movies[temp.keys()[0]] = temp[temp.keys()[0]]
3344 for movie in tmp_movies:
3345 if tmp_movies[movie][:-7].lower() == name or self.removeCommonWords(tmp_movies[movie][:-7]) == self.removeCommonWords(name):
3346 if year:
3347 if tmp_movies[movie][-5:-1] == year:
3348 if rtnyear:
3349 return {'name': tmp_movies[movie], u'sid': movie}
3350 else:
3351 return u"%07d" % int(movie) # Pad out IMDB# with leading zeroes
3352 IMDB_movies.append({'name': tmp_movies[movie], u'sid': movie})
3353
3354 if len(IMDB_movies) == 1: # If this is the only choice and titles matched then auto pick it
3355 if self.removeCommonWords(IMDB_movies[0]['name'][:-7]) == self.removeCommonWords(name):
3356 if rtnyear:
3357 return IMDB_movies[0]
3358 else:
3359 return u"%07d" % int(IMDB_movies[0][u'sid'])
3360
3361 # Does IMDB list this movie?
3362 if len(IMDB_movies) == 0:
3363 return False
3364
3365 # Did the user want an interactive interface?
3366 if not self.config['interactive']:
3367 return False
3368
3369 # Force only an IMDB look up for a movie
3370 movies = IMDB_movies
3371 video_type=u'IMDB'
3372
3373 ui = jamu_ConsoleUI(config = self.config, log = self.config['log'])
3374 try:
3375 inetref = ui.selectSeries(movies)
3376 except tvdb_userabort:
3377 if video_type==u'IMDB' or len(IMDB_movies) == 0:
3378 self._displayMessage(u"1-No selection made for Movie(%s)" % title)
3379 return False
3380 movies = IMDB_movies
3381 video_type=u'IMDB'
3382 try:
3383 inetref = ui.selectSeries(movies)
3384 except tvdb_userabort:
3385 self._displayMessage(u"2-No selection made for Movie(%s)" % title)
3386 return False
3387
3388 if inetref.has_key('sid'):
3389 if _can_int(inetref['sid']):
3390 if inetref['sid'] == '99999999':
3391 return inetref['sid']
3392 if rtnyear:
3393 if inetref['name'] == u'User input':
3394 try:
3395 data = imdb_access.get_movie(inetref['sid'])
3396 if data.has_key('long imdb title'):
3397 return {'name': data['long imdb title'], u'sid': inetref['sid']}
3398 elif data.has_key('title'):
3399 return {'name': data['title'], u'sid': inetref['sid']}
3400 else:
3401 return False
3402 except imdb._exceptions.IMDbDataAccessError:
3403 return False
3404 else:
3405 return inetref
3406 else:
3407 return u"%07d" % int(inetref['sid']) # Pad out IMDB# with leading zeroes
3408 else:
3409 return False
3410 else:
3411 return False
3412 # end _getTmdbIMDB
3413
3414 def _getTmdbGraphics(self, cfile, graphic_type, watched=False):
3415 '''Download either a movie Poster or Fanart
3416 return None
3417 return full qualified path and filename of downloaded graphic
3418 '''
3419 if graphic_type == u'-P':
3420 graphic_name = u'poster'
3421 key_type = u'coverart'
3422 rel_type = u'coverfile'
3423 else:
3424 graphic_name = u'fanart'
3425 key_type = u'fanart'
3426 rel_type = key_type
3427
3428 self.config['series_name']=cfile['file_seriesname']
3429 try:
3430 if len(cfile['inetref']) == 7: # IMDB number
3431 results = self.config['tmdb_api'].searchIMDB(cfile['inetref'])
3432 else:
3433 results = self.config['tmdb_api'].searchTMDB(cfile['inetref'])
3434 except TmdbMovieOrPersonNotFound, e:
3435 self._displayMessage(u"0-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref']))
3436 return None
3437 except Exception, e:
3438 self._displayMessage(u"themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile['file_seriesname'], graphic_name, e))
3439 return None
3440
3441 if results != None:
3442 if not results.has_key(key_type):
3443 self._displayMessage(u"1-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref']))
3444 return None
3445 else:
3446 self._displayMessage(u"1b-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref']))
3447 return None
3448
3449 graphic_file = (results[key_type].split(u','))[0].strip() # Only want the first image URL
3450
3451 self.config['g_defaultname']=False
3452 self.config['toprated'] = True
3453 self.config['nokeys'] = False
3454
3455 self.config['sid']=None
3456 if watched:
3457 if self.program_seriesid == None:
3458 self.config['g_series'] = self.sanitiseFileName(cfile['file_seriesname'])+u' Season 1'+self.graphic_suffix[rel_type]+u'.%(ext)s'
3459 else:
3460 self.config['g_series'] = self.sanitiseFileName(self.program_seriesid)+self.graphic_suffix[rel_type]+u'.%(ext)s'
3461 else:
3462 self.config['g_series'] = cfile['inetref']+self.graphic_suffix[rel_type]+u'.%(ext)s'
3463 if graphic_type == '-P':
3464 g_type = u'poster'
3465 else:
3466 g_type = u'fanart'
3467
3468 self.config['season_num']= None # Needed to get graphics named in 'g_series' format
3469
3470 self.config['overwrite'] = True # Force overwriting any existing graphic file
3471
3472 tmp_URL = graphic_file.replace(u"http://", u"")
3473 graphic_file = u"http://"+urllib.quote(tmp_URL.encode("utf-8"))
3474 value = self._downloadGraphics(u"%s:%s" % (g_type, graphic_file), mythtv=True)
3475
3476 self.config['overwrite'] = False # Turn off overwriting
3477
3478 if value == None:
3479 self._displayMessage(u"2-tmdb %s for Movie not found(%s)(%s)" % (graphic_name, cfile['filename'], cfile['inetref']))
3480 return None
3481 else:
3482 return self.rtnRelativePath(value, graphicsDirectories[rel_type])
3483 # end _getTmdbGraphics
3484
3485 def _getSecondarySourceGraphics(self, cfile, graphic_type, watched=False):
3486 '''Download from secondary source such as movieposter.com
3487 return None
3488 return full qualified path and filename of downloaded graphic
3489 '''
3490 if not len(self.config['myth_secondary_sources']):
3491 return None
3492
3493 if graphic_type == u'coverfile':
3494 graphic_type = u'poster'
3495 rel_type = u'coverfile'
3496
3497 if cfile['seasno'] == 0 and cfile['epno'] == 0:
3498 if not self.config['myth_secondary_sources'].has_key('movies'):
3499 return None
3500 if self.config['myth_secondary_sources']['movies'].has_key(graphic_type):
3501 source = self.config['myth_secondary_sources']['movies'][graphic_type]
3502 if source.find(u'%(imdb)s') != -1:
3503 if len(cfile['inetref']) != 7:
3504 try:
3505 results = self.config['tmdb_api'].searchTMDB(cfile['inetref'])
3506 except TmdbMovieOrPersonNotFound, e:
3507 self._displayMessage(u"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile['file_seriesname'], graphic_type, e))
3508 return None
3509 except Exception, e:
3510 self._displayMessage(u"\n! Warning: Secondary themoviedb.com error for Movie(%s) graphics(%s), error(%s)" % (cfile['file_seriesname'], graphic_type, e))
3511 return None
3512 if results == None:
3513 return None
3514 if not results.has_key('imdb'):
3515 self._displayMessage(u"\n! Warning: themoviedb.com wiki does not have an IMDB number to search a secondary source (%s)\nfor the movie (%s) inetref (%s).\n" % (source , cfile['filename'], cfile['inetref']))
3516 return None
3517 cfile['imdb'] = results['imdb']
3518 else:
3519 cfile['imdb'] = cfile['inetref']
3520 else:
3521 return None
3522 else:
3523 if not self.config['myth_secondary_sources'].has_key('tv'):
3524 return None
3525 if self.config['myth_secondary_sources']['tv'].has_key(graphic_type):
3526 source = self.config['myth_secondary_sources']['tv'][graphic_type]
3527 else:
3528 return None
3529
3530 self.config['series_name']=cfile['file_seriesname']
3531
3532 if self.config['simulation']:
3533 sys.stdout.write(u"Simulating - downloading Secondary Source graphic (%s)\n" % cfile['file_seriesname'])
3534 return u"Simulated Secondary Source graphic filename place holder"
3535
3536 # Test that the secondary's required data has been passed
3537 try:
3538 command = source % cfile
3539 except:
3540 self._displayMessage(u"Graphics Secondary source command:\n%s\nRequired information is not available. Here are the variables that are available:\n%s\n" % (source, cfile))
3541 return None
3542
3543 tmp_files = callCommandLine(command)
3544 if tmp_files == '':
3545 self._displayMessage(u"\n! Warning: Source (%s)\n could not find (%s) for (%s)(%s)\n" % (source % cfile, graphic_type, cfile['filename'], cfile['inetref']))
3546 return None
3547
3548 tmp_array=tmp_files.split('\n')
3549 if tmp_array[0].startswith(u'Failed'):
3550 self._displayMessage(u"\n! Warning: Source (%s)\nfailed to download (%s) for (%s)(%s)\n" % (source % cfile, graphic_type, cfile['filename'], cfile['inetref']))
3551 return None
3552
3553 if tmp_array[0].startswith(u'file://'):
3554 tmp_files=tmp_array[0].replace(u'file://', u'')
3555 if not os.path.isfile(tmp_files):
3556 sys.stderr.write(u'\n! Error: The graphic file does not exist (%s)\n' % tmp_files)
3557 sys.exit(1)
3558
3559 # Fix file extentions in all caps or 4 character JPEG extentions
3560 fileExtension = (_getExtention(tmp_files)).lower()
3561 if fileExtension == u'jpeg':
3562 fileExtension = u'jpg'
3563 if watched:
3564 if self.program_seriesid == None:
3565 filename = u'%s/%s%s.%s' % (self.config['posterdir'][0], self.sanitiseFileName(cfile['file_seriesname']), self.graphic_suffix[rel_type], fileExtension)
3566 else:
3567 filename = u'%s/%s%s.%s' % (self.config['posterdir'][0], self.sanitiseFileName(self.program_seriesid), self.graphic_suffix[rel_type], fileExtension)
3568 else:
3569 filename = u'%s/%s%s.%s' % (self.config['posterdir'][0], cfile['inetref'], self.graphic_suffix[rel_type], fileExtension)
3570
3571 if os.path.isfile(filename): # This may be the same small file or worse then current
3572 try:
3573 (width, height) = self.config['image_library'].open(filename).size
3574 (width2, height2) = self.config['image_library'].open(tmp_files).size
3575 if width >= width2:
3576 os.remove(tmp_files)
3577 return None
3578 except IOError:
3579 return None
3580
3581 # Verify that the downloaded file was NOT HTML instead of the intended file
3582 if self._checkValidGraphicFile(tmp_files, graphicstype=u'', vidintid=False) == False:
3583 os.remove(tmp_files) # Delete the useless HTML text
3584 return None
3585 shutil.copy2(tmp_files, filename)
3586 os.remove(tmp_files)
3587 self.num_secondary_source_graphics_downloaded+=1
3588 return self.rtnRelativePath(filename, graphicsDirectories[rel_type])
3589 else:
3590 graphic_file = tmp_array[0]
3591
3592 self.config['g_defaultname']=False
3593 self.config['toprated'] = True
3594 self.config['nokeys'] = False
3595
3596 self.config['sid']=None
3597 if watched:
3598 if self.program_seriesid == None:
3599 self.config['g_series'] = self.sanitiseFileName(cfile['file_seriesname'])+self.graphic_suffix[rel_type]+'.%(ext)s'
3600 else:
3601 self.config['g_series'] = self.sanitiseFileName(self.program_seriesid)+self.graphic_suffix[rel_type]+'.%(ext)s'
3602 else:
3603 self.config['g_series'] = self.sanitiseFileName(cfile['inetref'])+self.graphic_suffix[rel_type]+'.%(ext)s'
3604 g_type = graphic_type
3605
3606 self.config['season_num']= None # Needed to get graphics named in 'g_series' format
3607
3608 self.config['overwrite'] = True # Force overwriting any existing graphic file
3609
3610 tmp_URL = graphic_file.replace(u"http://", u"")
3611 graphic_file = u"http://"+urllib.quote(tmp_URL.encode("utf-8"))
3612 value = self._downloadGraphics(u"%s:%s" % (g_type, graphic_file), mythtv=True)
3613
3614 self.config['overwrite'] = False # Turn off overwriting
3615 if value == None:
3616 self._displayMessage(u"Secondary source %s not found(%s)(%s)" % (graphic_file, cfile['filename'], cfile['inetref']))
3617 return None
3618 else:
3619 self.num_secondary_source_graphics_downloaded+=1
3620 return self.rtnRelativePath(value, graphicsDirectories[rel_type])
3621 # end _getSecondarySourceGraphics
3622
3623 def combineMetaData(self, available_metadata, meta_dict, vid_type=False):
3624 ''' Combine the current data with new meta data from primary or secondary sources
3625 return combinted meta data dictionary
3626 '''
3627 # Combine meta data
3628 for key in meta_dict.keys():
3629 if key in self.config['metadata_exclude_as_update_trigger']:
3630 continue
3631 else:
3632 if key == 'inetref' and available_metadata[key] != meta_dict[key]:
3633 available_metadata[key] = meta_dict[key]
3634 continue
3635 if key == 'releasedate' and available_metadata[key] != meta_dict[key]:
3636 available_metadata[key] = meta_dict[key]
3637 continue
3638 if key == 'userrating' and available_metadata[key] == 0.0:
3639 available_metadata[key] = meta_dict[key]
3640 continue
3641 if key == 'length' and available_metadata[key] == 0:
3642 available_metadata[key] = meta_dict[key]
3643 continue
3644 if key == 'rating' and (available_metadata[key] == 'NR' or available_metadata[key] == 'Unknown'):
3645 available_metadata[key] = meta_dict[key]
3646 continue
3647 if key == 'year' and available_metadata[key] == 1895:
3648 available_metadata[key] = meta_dict[key]
3649 continue
3650 if key == 'category' and available_metadata[key] == 0:
3651 available_metadata[key] = meta_dict[key]
3652 continue
3653 if key == 'inetref' and available_metadata[key] == '00000000':
3654 available_metadata[key] = meta_dict[key]
3655 continue
3656 if key == 'title':
3657 available_metadata[key] = meta_dict[key]
3658 continue
3659 if vid_type and key == 'subtitle': # There are no subtitles in movies
3660 continue
3661 if key == 'plot': # Remove any line-feeds from the plot. Mythvideo does not expect them.
3662 meta_dict[key] = meta_dict[key].replace('\n', ' ')
3663 if (vid_type and key == 'plot') and (meta_dict[key].find('@') != -1 or len(meta_dict[key].split(' ')) < 10):
3664 continue
3665 if vid_type and key == 'plot':
3666 if available_metadata[key] != None:
3667 if len(available_metadata[key].split(' ')) < 10 and len(meta_dict[key].split(' ')) > 10:
3668 available_metadata[key] = meta_dict[key]
3669 continue
3670 if not available_metadata.has_key(key): # Mainly for Genre, Cast and Countries
3671 available_metadata[key] = meta_dict[key]
3672 continue
3673 if available_metadata[key] == None or available_metadata[key] == '' or available_metadata[key] == 'None' or available_metadata[key] == 'Unknown':
3674 available_metadata[key] = meta_dict[key]
3675 continue
3676 return available_metadata
3677 # end combineMetaData
3678
3679
3680 def _getSecondarySourceMetadata(self, cfile, available_metadata):
3681 '''Download meta data from secondary source
3682 return available_metadata (returns the current metadata unaltered)
3683 return dictionary of combined meta data
3684 '''
3685 if not len(self.config['myth_secondary_sources']):
3686 return available_metadata
3687
3688 if cfile['seasno'] == 0 and cfile['epno'] == 0:
3689 if not self.config['myth_secondary_sources'].has_key('movies'):
3690 return available_metadata
3691 movie = True
3692 if self.config['myth_secondary_sources']['movies'].has_key('metadata'):
3693 source = self.config['myth_secondary_sources']['movies']['metadata']
3694 if source.find(u'%(imdb)s') != -1:
3695 if len(cfile['inetref']) != 7:
3696 try:
3697 results = self.config['tmdb_api'].searchTMDB(cfile['inetref'])
3698 except TmdbMovieOrPersonNotFound, e:
3699 self._displayMessage(u"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile['file_seriesname'], e))
3700 return available_metadata
3701 except Exception, e:
3702 self._displayMessage(u"Secondary metadata themoviedb.com error for Movie(%s), error(%s)" % (cfile['file_seriesname'], e))
3703 return available_metadata
3704 if results == None:
3705 return available_metadata
3706 if not results.has_key('imdb'):
3707 self._displayMessage(u"No IMDB number for meta data secondary source (%s)\nfor the movie (%s) inetref (%s) in themoviedb.com wiki.\n" % (source, cfile['filename'], cfile['inetref']))
3708 return available_metadata
3709 cfile['imdb'] = results['imdb']
3710 else:
3711 cfile['imdb'] = cfile['inetref']
3712 else:
3713 return available_metadata
3714 else:
3715 if not self.config['myth_secondary_sources'].has_key('tv'):
3716 return available_metadata
3717 movie = False
3718 if self.config['myth_secondary_sources']['tv'].has_key('metadata'):
3719 source = self.config['myth_secondary_sources']['tv']['metadata']
3720 else:
3721 return available_metadata
3722
3723 # Test that the secondary's required data has been passed
3724 try:
3725 command = source % cfile
3726 except:
3727 self._displayMessage(u"Metadata Secondary source command:\n%s\nRequired information is not available. Here are the variables that are available:\n%s\n" % (source, cfile))
3728 return available_metadata
3729
3730 self.config['series_name']=cfile['file_seriesname']
3731
3732 tmp_files=u''
3733 tmp_files = (callCommandLine(command)).decode("utf8")
3734 if tmp_files == '':
3735 self._displayMessage(u"1-Secondary source (%s)\ndid not find(%s)(%s) meta data dictionary cannot be returned" % (source % cfile, cfile['filename'], cfile['inetref']))
3736 return available_metadata
3737
3738 meta_dict={}
3739 tmp_array=tmp_files.split('\n')
3740 for element in tmp_array:
3741 element = (element.rstrip('\n')).strip()
3742 if element == '' or element == None:
3743 continue
3744 try:
3745 index = element.index(':')
3746 except:
3747 continue
3748 key = element[:index].lower()
3749 data = element[index+1:]
3750 if data == None or data == '':
3751 continue
3752 if key == u'inetref' and len(cfile['inetref']) == 7:
3753 meta_dict[key] = cfile['inetref']
3754 continue
3755 data = self._changeAmp(data)
3756 data = self._changeToCommas(data)
3757 if key == 'year':
3758 try:
3759 meta_dict[key] = int(data)
3760 except:
3761 continue
3762 continue
3763 if key == 'userrating':
3764 try:
3765 meta_dict[key] = float(data)
3766 except:
3767 continue
3768 continue
3769 if key == 'runtime':
3770 try:
3771 meta_dict['length'] = long(data)
3772 except:
3773 continue
3774 continue
3775 if key == 'movierating':
3776 meta_dict['rating'] = data
3777 continue
3778 if key == 'plot':
3779 try:
3780 if len(data.split(' ')) < 10: # Skip plots that are less than 10 words
3781 continue
3782 except:
3783 pass
3784 if key == 'trailer':
3785 continue
3786 if key == 'releasedate':
3787 try:
3788 meta_dict[key] = datetime.datetime.strptime(data,'%Y-%m-%d').date()
3789 except ValueError:
3790 pass
3791 continue
3792 meta_dict[key] = data
3793 if not len(meta_dict):
3794 self._displayMessage(u"2-Secondary source (%s)\n did not find(%s)(%s) meta data dictionary cannot be returned" % (source % cfile, cfile['filename'], cfile['inetref']))
3795 return available_metadata
3796
3797 # Combine meta data
3798 available_metadata = self.combineMetaData(available_metadata, meta_dict, vid_type=movie)
3799 self.num_secondary_source_metadata_downloaded+=1
3800 return available_metadata
3801 # end _getSecondarySourceMetadata
3802
3803 def _getTmdbMetadata(self, cfile, available_metadata):
3804 '''Download a movie's meta data and massage the genres string
3805 return results for secondary sources when no primary source meta data
3806 return dictionary of metadata combined with data from a secondary source
3807 '''
3808 try:
3809 if len(cfile['inetref']) == 7: # IMDB number
3810 meta_dict = self.config['tmdb_api'].searchIMDB(cfile['inetref'])
3811 else:
3812 meta_dict = self.config['tmdb_api'].searchTMDB(cfile['inetref'])
3813 except TmdbMovieOrPersonNotFound, e:
3814 self._displayMessage(u"0-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile['filename'], cfile['inetref']))
3815 return self._getSecondarySourceMetadata(cfile, available_metadata)
3816 except Exception, e:
3817 self._displayMessage(u"themoviedb.com error for Movie(%s)(%s) meta data dictionary cannot be returned, error(%s)" % (cfile['filename'], cfile['inetref'], e))
3818 return self._getSecondarySourceMetadata(cfile, available_metadata)
3819
3820 if meta_dict == None:
3821 self._displayMessage(u"1-tmdb Movie not found(%s)(%s) meta data dictionary cannot be returned" % (cfile['filename'], cfile['inetref']))
3822 return self._getSecondarySourceMetadata(cfile, available_metadata)
3823
3824 keys = meta_dict.keys()
3825
3826 for key in keys:
3827 data = meta_dict[key]
3828 if not data:
3829 continue
3830 if key == 'homepage':
3831 continue
3832 data = self._changeAmp(data)
3833 data = self._changeToCommas(data)
3834 if key == 'genres':
3835 genres=''
3836 genre_array = data.split(',')
3837 for i in range(len(genre_array)):
3838 genre_array[i] = (genre_array[i].strip()).lower()
3839 if genre_array[i] in self.config['tmdb_genre_filter']:
3840 genres+=genre_array[i].title()+','
3841 if genres == '':
3842 meta_dict[key] = u''
3843 continue
3844 else:
3845 meta_dict[key] = genres[:-1]
3846 if key == 'trailer':
3847 continue
3848 if key == 'year':
3849 try:
3850 meta_dict[key] = int(data)
3851 except:
3852 pass
3853 continue
3854 if key == 'userrating':
3855 try:
3856 meta_dict[key] = float(data)
3857 except:
3858 pass
3859 continue
3860 if key == 'url':
3861 meta_dict['homepage'] = data
3862 continue
3863 if key == 'releasedate':
3864 try:
3865 meta_dict[key] = datetime.datetime.strptime(data,'%Y-%m-%d').date()
3866 except ValueError:
3867 del meta_dict[key]
3868 continue
3869 if key == 'runtime':
3870 try:
3871 meta_dict['length'] = long(data)
3872 except:
3873 pass
3874 continue
3875 if key == 'movierating':
3876 meta_dict['rating'] = data
3877 continue
3878 if meta_dict.has_key('rating'):
3879 if meta_dict['rating'] == '':
3880 meta_dict['rating'] = 'Unknown'
3881
3882 if len(meta_dict):
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)
3888 else:
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
3892
3893 def _getTvdbGraphics(self, cfile, graphic_type, toprated=False, watched=False):
3894 '''Download either a TV Series Poster, Banner, Fanart or Episode image
3895 return None
3896 return full qualified path and filename of downloaded graphic
3897 '''
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'
3903
3904 self.config['g_defaultname']=False
3905 self.config['toprated'] = toprated
3906 self.config['nokeys'] = False
3907 self.config['maximum'] = u'1'
3908
3909 if watched:
3910 self.config['sid']=cfile['inetref']
3911 else:
3912 self.config['sid']=None
3913 self.config['episode_name'] = None
3914 self.config['series_name']=cfile['file_seriesname']
3915 if not watched:
3916 self.config['season_num']=u"%d" % cfile['seasno']
3917 self.config['episode_num']=u"%d" % cfile['epno']
3918
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']:
3921 try:
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))
3925 return None
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']
3930 else:
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'])
3932 return None
3933 else:
3934 if not self.verifySeriesExists():
3935 self._displayMessage(u"tvdb Series not found(%s)" % cfile['filename'])
3936 return None
3937
3938 if watched:
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'
3942 else:
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'
3945 else:
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'
3950 if toprated:
3951 typegetGraphics=self.getTopRatedGraphics
3952 self.config['season_num']= None # Needed to get toprated graphics named in 'g_series' format
3953 else:
3954 typegetGraphics=self.getGraphics
3955
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
3959 if value == None:
3960 return None
3961 else:
3962 return self.rtnRelativePath(value, graphicsDirectories[rel_type])
3963 # end _getTvdbGraphics
3964
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
3969 '''
3970 global video_type, UI_title
3971 video_type=u'TV series'
3972 UI_title = cfile['file_seriesname']
3973
3974 meta_dict={}
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()
3984
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']:
3987 try:
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))
3991 return None
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']
3996 else:
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'])
3998 return False
3999 else:
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)
4003
4004 if self.config['sid'] == '99999999':
4005 if not self.config['interactive']:
4006 return self._getSecondarySourceMetadata(cfile, available_metadata)
4007 else:
4008 return {'sid': self.config['sid'], 'title': cfile['file_seriesname']}
4009
4010 meta_dict={}
4011 tmp_array=(self.getSeriesEpisodeData()).split('\n')
4012
4013 for element in tmp_array:
4014 element = (element.rstrip('\n')).strip()
4015 if element == '':
4016 continue
4017 index = element.index(':')
4018 key = element[:index].lower()
4019 data = element[index+1:]
4020 if data == None:
4021 continue
4022 if key == 'series':
4023 meta_dict['title'] = data
4024 continue
4025 if key == 'seasonnumber':
4026 try:
4027 meta_dict['season'] = int(data)
4028 except:
4029 pass
4030 continue
4031 if key == 'episodenumber':
4032 try:
4033 meta_dict['episode'] = int(data)
4034 except:
4035 pass
4036 continue
4037 if key == 'episodename':
4038 meta_dict['subtitle'] = data
4039 continue
4040 if key == u'overview':
4041 meta_dict['plot'] = data
4042 continue
4043 if key == u'director' and data == 'None':
4044 meta_dict['director'] = ''
4045 continue
4046 if key == u'firstaired' and len(data) > 4:
4047 try:
4048 meta_dict['year'] = int(data[:4])
4049 except:
4050 pass
4051 meta_dict['firstaired'] = data
4052 try:
4053 meta_dict['releasedate'] = datetime.datetime.strptime(data,'%Y-%m-%d').date()
4054 except ValueError:
4055 pass
4056 continue
4057 if key == 'year':
4058 try:
4059 meta_dict['year'] = int(data)
4060 except:
4061 pass
4062 continue
4063 if key == 'seriesid':
4064 meta_dict['inetref'] = data
4065 meta_dict[key] = data
4066 continue
4067 if key == 'rating':
4068 try:
4069 meta_dict['userrating'] = float(data)
4070 except:
4071 pass
4072 continue
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':
4076 try:
4077 meta_dict['length'] = long(data)
4078 except:
4079 pass
4080 continue
4081 meta_dict[key] = data
4082
4083 if len(meta_dict):
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():
4090 break
4091 else:
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)
4098 else:
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
4102
4103 def _make_db_ready(self, text):
4104 '''Prepare text for inclusion into a DB
4105 return None
4106 return data base ready text
4107 '''
4108 if not text: return text
4109 try:
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:
4118 pass
4119
4120 return text
4121 # end make_db_ready
4122
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
4128 '''
4129 if data_string == '':
4130 return True
4131 data = data_string.split(',')
4132 for i in range(len(data)):
4133 data[i]=data[i].strip()
4134 try:
4135 data.remove('')
4136 except:
4137 pass
4138
4139 if cast_genres_type == 'genres':
4140 for item in data:
4141 vim.genre.add(item)
4142 elif cast_genres_type == 'cast':
4143 for item in data:
4144 vim.cast.add(item)
4145 elif cast_genres_type == 'countries':
4146 for item in data:
4147 vim.country.add(item)
4148
4149 return True
4150 # end _addCastGenreCountry()
4151
4152 # Local variables
4153 errors = []
4154 new_names = []
4155
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
4160 '''
4161 wild_card = False
4162 org_src = src
4163 if src[-1:] == '*':
4164 wild_card = True
4165 (src, fileName) = os.path.split(src)
4166 try:
4167 names = os.listdir(unicode(src, 'utf8'))
4168 except (UnicodeEncodeError, TypeError):
4169 names = os.listdir(src)
4170 else:
4171 if os.path.isfile(src):
4172 (src, fileName) = os.path.split(src)
4173 names = [fileName]
4174 else:
4175 try:
4176 names = os.listdir(unicode(src, 'utf8'))
4177 except (UnicodeEncodeError, TypeError):
4178 names = os.listdir(src)
4179
4180 if ignore is not None:
4181 ignored_names = ignore(src, names)
4182 else:
4183 ignored_names = set()
4184
4185 try:
4186 if self.config['simulation']:
4187 sys.stdout.write(u"Simulation creating subdirectories for file move (%s)\n" % dst)
4188 else:
4189 self._displayMessage(u"Creating subdirectories for file move (%s)\n" % dst)
4190 os.makedirs(dst) # Some of the subdirectories may already exist
4191 except OSError:
4192 pass
4193
4194 for name in names:
4195 if name in ignored_names:
4196 continue
4197 srcname = os.path.join(src, name)
4198 dstname = os.path.join(dst, name)
4199
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))
4202 continue
4203 try:
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))
4208 else:
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):
4213 if wild_card:
4214 self._displayMessage(u"Wildcard skipping subdirectory (%s)\n" % srcname)
4215 continue
4216 self.num_created_video_subdirectories+=1
4217 self._displayMessage(u"Move subdirectory (%s)\n" % srcname)
4218 self._moveDirectoryTree(srcname, dstname, symlinks, ignore)
4219 else:
4220 if self.config['simulation']:
4221 if wild_card:
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)
4226 else:
4227 self._displayMessage(u"Simulation of wildcard skipping file(%s)" % (srcname,))
4228 else:
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)
4232 else:
4233 if wild_card:
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)
4239 else:
4240 self._displayMessage(u"Wildcard skipping file(%s)" % (srcname,))
4241 else:
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
4251 except:
4252 self.errors.append([src, dst, u"Unknown error"])
4253
4254 return [self.new_names, self.errors]
4255 # end _moveDirectoryTree
4256
4257 # local variable for move stats
4258 num_moved_video_files=0
4259 num_created_video_subdirectories=0
4260 num_symbolic_links=0
4261
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
4269 """
4270 global UI_selectedtitle
4271 # Validate that the targets and destinations actually exist.
4272 count=1
4273 for file_dir in target_destination_array:
4274 if os.access(file_dir, os.F_OK | os.R_OK):
4275 if count % 2 == 0:
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,))
4279 sys.exit(1)
4280 else:
4281 tmp_dir = file_dir
4282 for directory in self.config['mythvideo']:
4283 dummy_dir = file_dir.replace(directory, u'')
4284 if dummy_dir != tmp_dir:
4285 break
4286 else:
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'], ))
4288 sys.exit(1)
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:
4296 break
4297 else:
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'],))
4299 sys.exit(1)
4300 count+=1
4301
4302 # Stats counters
4303 num_renamed_files = 0
4304 num_mythdb_updates = 0
4305
4306 i = 0
4307 video_files_to_process=[]
4308 cfile_array=[]
4309 while i < len(target_destination_array):
4310 src = target_destination_array[i]
4311 wild_card = False
4312 if src[-1:] == u'*':
4313 org_src = src
4314 wild_card = True
4315 (src, fileName) = os.path.split(src)
4316 dst = target_destination_array[i+1]
4317 self.errors = []
4318 self.new_names = []
4319 if wild_card:
4320 results = self._moveDirectoryTree(org_src, dst, symlinks=False, ignore=None)
4321 else:
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]))
4327 tmp_cfile_array=[]
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)
4333 else:
4334 for dictionary in self._processNames([file_name], verbose = self.config['debug_enabled'], movies=True):
4335 tmp_cfile_array.append(dictionary)
4336
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)
4346 if result == None:
4347 intid = result
4348 else:
4349 intid = result.intid
4350 if not intid:
4351 result = mythvideo.getVideo(exactfile=self.movie_file_format % (tmp_path, cfile['filename'], cfile['ext']), host=localhostname.lower())
4352 if result == None:
4353 intid = result
4354 else:
4355 intid = result.intid
4356 if intid:
4357 metadata = Video(intid, db=mythvideo)
4358 if tmp_filename[0] == '/':
4359 host = u''
4360 self.absolutepath = True
4361 else:
4362 host = localhostname.lower()
4363 self.absolutepath = False
4364
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))
4367 else:
4368 self._displayMessage(u"Mythdb update for old file:\n(%s) new:\n(%s)\n" % (video_file, tmp_filename))
4369 Video(intid, db=mythvideo).update({'filename': tmp_filename, 'host': host})
4370 num_mythdb_updates+=1
4371 break
4372 else:
4373 pass
4374 cfile_array.extend(tmp_cfile_array)
4375 i+=2 # Increment by 2 because array is int pairs of target and destination
4376
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
4384 sid = None
4385 new_filename = u''
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()]
4389 if not sid:
4390 data = self._getTmdbIMDB(cfile['file_seriesname'], rtnyear=True)
4391 if data:
4392 sid = data[u'sid']
4393 if data[u'sid'] == '99999999': # The user chose to ignore this video
4394 continue
4395 new_filename = self.sanitiseFileName(data[u'name'])
4396 else:
4397 continue
4398 else:
4399 imdb_access = imdb.IMDb()
4400 try:
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'])
4406 else:
4407 continue
4408 except imdb._exceptions.IMDbDataAccessError:
4409 continue
4410
4411 if not sid: # Cannot find this movie skip the renaming
4412 continue
4413 inetref = sid
4414 if not new_filename:
4415 continue
4416 else:
4417 cfile_array[index]['file_seriesname'] = new_filename
4418 else: # File rename for a TV Series Episode
4419 UI_selectedtitle = u''
4420 new_filename = 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
4433 continue
4434
4435 if new_filename:
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'])
4438 continue
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))
4443 else:
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)
4446 continue
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)
4453 if result == None:
4454 intid = result
4455 else:
4456 intid = result.intid
4457 if not intid:
4458 result = mythvideo.getVideo(exactfile=self.movie_file_format % (cfile['filepath'], cfile['filename'], cfile['ext']), host=localhostname.lower())
4459 if result == None:
4460 intid = result
4461 else:
4462 intid = result.intid
4463 if tmp_filename[0] == '/':
4464 host = u''
4465 self.absolutepath = True
4466 else:
4467 host = localhostname.lower()
4468 self.absolutepath = False
4469 if intid:
4470 metadata = Video(intid, db=mythvideo)
4471 if self.config['simulation']:
4472 sys.stdout.write(u"Simulation Mythdb update for renamed file(%s)\n" % (tmp_filename))
4473 else:
4474 self._displayMessage(u"Mythdb update for renamed file(%s)\n" % (tmp_filename))
4475 Video(intid, db=mythvideo).update({'filename': tmp_filename, 'host': host})
4476 else:
4477 if self.config['simulation']:
4478 sys.stdout.write(u"Simulation Mythdb add for renamed file(%s)\n" % (tmp_filename))
4479 else:
4480 self._displayMessage(u"Adding Mythdb record for file(%s)\n" % (tmp_filename))
4481 initrec = {}
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
4488
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))
4492
4493 return cfile_array
4494 # end _moveVideoFiles
4495
4496 def _displayMessage(self, message):
4497 """Displays messages through stdout. Usually used with MythTv metadata updates in -V
4498 verbose mode.
4499 returns nothing
4500 """
4501 if message[-1:] != '\n':
4502 message+='\n'
4503 if self.config['mythtv_verbose']:
4504 sys.stdout.write(message)
4505 # end _displayMessage
4506
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
4511 '''
4512 directories=self.config['mythvideo']
4513
4514 if not len(directories):
4515 sys.stderr.write(u"\n! Error: There must be a video directory specified in MythTv\n")
4516 sys.exit(1)
4517
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?
4521 return None
4522
4523 missing_list=[]
4524 for cfile in validFiles:
4525 try:
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'])
4529
4530 # Find the MythTV meta data
4531 result = mythvideo.getVideo(exactfile=videopath)
4532 if result == None:
4533 intid = result
4534 else:
4535 intid = result.intid
4536 if not intid:
4537 result = mythvideo.getVideo(exactfile=self.rtnRelativePath(videopath, 'mythvideo'), host=localhostname.lower())
4538 if result == None:
4539 intid = result
4540 else:
4541 intid = result.intid
4542 if intid == None:
4543 missing_list.append(cfile)
4544 else:
4545 meta_dict = Video(intid, db=mythvideo)
4546 if self.config['video_dir']:
4547 if not mythvideo.getVideo(exactfile=meta_dict[u'filename'], host=meta_dict[u'host']):
4548 missing_list.append(cfile)
4549 continue
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'] == '':
4552 continue
4553 missing_list.append(cfile)
4554
4555 return missing_list
4556 # end _findMissingInetref
4557
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
4562 '''
4563 # Verify that the graphics file is NOT HTML instead of the intended graphics file
4564 try:
4565 p = subprocess.Popen(u'file "%s"' % filename, shell=True, bufsize=4096, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
4566 except:
4567 # There is something wrong with the file but do NOT say it is invalid just in case!
4568 return True
4569 data = p.stdout.readline()
4570 try:
4571 data = data.encode('utf8')
4572 except UnicodeDecodeError:
4573 data = unicode(data,'utf8')
4574 index = data.find(u'HTML document text')
4575 if index == -1:
4576 return True
4577 elif self.config['simulation']:
4578 sys.stdout.write(
4579 u"Simulation deleting bad graphics file (%s) as it is really HTML\n" % (filename, )
4580 )
4581 if vidintid:
4582 sys.stdout.write(
4583 u"and the MythVideo record was corrected for the graphic reference.\n"
4584 )
4585 return False
4586 else:
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))
4589 if vidintid:
4590 repair = {}
4591 if graphicstype == u'coverfile':
4592 repair[graphicstype] = u'No Cover'
4593 else:
4594 repair[graphicstype] = u''
4595 Video(vidintid, db=mythvideo).update(repair)
4596 return False
4597 # end _checkValidGraphicFile()
4598
4599
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
4603 report.
4604 '''
4605 global localhostname
4606 num_total = 0
4607 num_deleted = 1
4608 num_new_total = 2
4609 stats = {'coverfile': [0,0,0], 'banner': [0,0,0], 'fanart': [0,0,0]}
4610
4611 graphics_file_dict={}
4612 all_graphics_file_list=[]
4613 for directory in graphicsDirectories.keys():
4614 if directory == 'screenshot':
4615 continue
4616 file_list = _getFileList(self.config[graphicsDirectories[directory]])
4617 if not len(file_list):
4618 graphics_file_dict[directory] = []
4619 continue
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)
4623 continue
4624 g_ext = _getExtention(g_file)
4625 if not g_ext in self.image_extensions:
4626 file_list.remove(g_file)
4627 continue
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
4632
4633 for key in graphicsDirectories.keys(): # Set initial totals
4634 if key == 'screenshot':
4635 continue
4636 stats[key][num_total] = len(graphics_file_dict[key])
4637
4638 # Start reading videometadata records to remove their graphics from the image orphan list
4639 try:
4640 records = mythvideo.searchVideos()
4641 except MythError, e:
4642 sys.stderr.write(u"\n! Error: Reading all videometadata records: %s\n" % e.args[0])
4643 return
4644
4645 atleast_one_video_file = False
4646 if records:
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():
4653 continue
4654 # Start removing any graphics in this videometadata record
4655 for key in meta_dict.keys():
4656 if key in ['host','filename','intid', 'inetref']:
4657 continue
4658 if meta_dict[key] in [None, u'', u'None', u'No Cover', u'Unknown']:
4659 continue
4660
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
4665 else:
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
4670 continue
4671
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']
4677
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])
4689
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")
4692 return
4693 # end reading videometadata records to remove their graphics from the image orphan list
4694
4695 # Get Scheduled and Recorded program list
4696 programs = self._getScheduledRecordedProgramList()
4697
4698 # Remove Scheduled and Recorded program's graphics files from the delete list
4699 if programs:
4700 for field in graphicsDirectories.keys():
4701 if field == 'screenshot':
4702 continue
4703 remove=[]
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)
4710 break
4711 if not isValidPosixFilename(program['title']) and program['seriesid'] != u'':
4712 if fileBaseName.lower().startswith(program['seriesid'].lower()):
4713 remove.append(graphic)
4714 break
4715 for rem in remove:
4716 if self._checkValidGraphicFile(rem, graphicstype=u'', vidintid=False) == True:
4717 graphics_file_dict[field].remove(rem)
4718 try:
4719 all_graphics_file_list.remove(rem)
4720 except ValueError, e:
4721 pass
4722
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)
4727 continue
4728 if filel.endswith('mirobridge_banner.jpg'):
4729 all_graphics_file_list.remove(filel)
4730 continue
4731 if filel.endswith('mirobridge_fanart.jpg'):
4732 all_graphics_file_list.remove(filel)
4733 continue
4734
4735 for key in graphicsDirectories.keys(): # Set deleted files totals
4736 if key == 'screenshot':
4737 continue
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])
4743
4744 # Delete all graphics files still on the delete list
4745 for filel in all_graphics_file_list:
4746 if self.config['simulation']:
4747 sys.stdout.write(
4748 u"Simulation deleting (%s)\n" % (filel)
4749 )
4750 else:
4751 try:
4752 os.remove(filel)
4753 except OSError:
4754 pass
4755 self._displayMessage(u"(%s) Has been deleted\n" % (filel))
4756
4757 for key in graphicsDirectories.keys(): # Set new files totals
4758 if key == 'screenshot':
4759 continue
4760 stats[key][num_new_total] = stats[key][num_total] - stats[key][num_deleted]
4761
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':
4769 continue
4770 if key == 'coverfile':
4771 g_type = 'posters'
4772 else:
4773 g_type = key+'s'
4774 sys.stdout.write(u'% 9s % 7s ......................(% 5d)\n' % (stat_type[index], g_type, stats[key][index], ))
4775
4776 for key in graphicsDirectories.keys(): # Print stats
4777 if key == 'screenshot':
4778 continue
4779 if not len(graphics_file_dict[key]):
4780 continue
4781 if key == 'coverfile':
4782 g_type = 'poster'
4783 else:
4784 g_type = key
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)
4788 return
4789 # end _graphicsCleanup
4790
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
4795 '''
4796 if not self.config['ffmpeg']:
4797 return False
4798
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']:
4801 return False
4802
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)
4804
4805 ffmpeg_found = True
4806 while 1:
4807 data = p.stderr.readline()
4808 if data.endswith('not found\n'):
4809 ffmpeg_found = False
4810 break
4811 if data.startswith(' Duration:'):
4812 break
4813 if data == '' and p.poll() != None:
4814 break
4815
4816 if ffmpeg_found == False:
4817 self.config['ffmpeg'] = False
4818 return False
4819 elif data:
4820 time = (data[data.index(':')+1: data.index('.')]).strip()
4821 return (60*(int(time[:2]))+(int(time[3:5])))
4822 else:
4823 return False
4824 # end _getVideoLength
4825
4826
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
4832 """
4833 global localhostname
4834 intids = []
4835 try:
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])
4839 sys.exit(1)
4840 if records:
4841 for record in records:
4842 intids.append(record.intid)
4843
4844 videometadatarecords=[]
4845 if len(intids):
4846 for intid in intids:
4847 vidrec = Video(intid, db=mythvideo)
4848 if vidrec[u'host'] != u'' and vidrec[u'host'] != None:
4849 if vidrec[u'host'].lower() != localhostname.lower():
4850 continue
4851 videometadatarecords.append(vidrec)
4852
4853 return videometadatarecords
4854 else:
4855 return None
4856 # end _getMiroVideometadataRecords()
4857
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
4861 '''
4862 extradata = {}
4863 extradata[u'intid'] = [mythvideorec[u'intid']]
4864 if vidtype == u'movies':
4865 extradata[u'tv'] = False
4866 else:
4867 extradata[u'tv'] = True
4868
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
4873 continue
4874 elif key == u'coverfile': # Look for undersized coverart
4875 if mythvideorec[u'filename'][0] == u'/':
4876 self.absolutepath = True
4877 else:
4878 self.absolutepath = False
4879 filename = self.rtnAbsolutePath(mythvideorec[key], graphicsDirectories[key])
4880 try:
4881 (width, height) = self.config['image_library'].open(filename).size
4882 if width < self.config['min_poster_size']:
4883 extradata[key] = False
4884 continue
4885 except IOError:
4886 extradata[key] = False
4887 continue
4888 continue
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
4894 continue
4895
4896 if vidtype == u'movies': # Data specific to Movie Trailers
4897 if mythvideorec[u'filename'][0] == u'/':
4898 self.absolutepath = True
4899 else:
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
4905 else:
4906 extradata[u'symlink'] = False
4907 moviename = mythvideorec['subtitle']
4908 if not moviename:
4909 moviename = ''
4910 else:
4911 index = moviename.find(self.config[u'mb_movies'][filter(is_not_punct_char, mythvideorec[u'title'].lower())])
4912 if index != -1:
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"))
4918 years = []
4919 i = 0
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)))
4922 i+=1
4923 imdb_access = imdb.IMDb()
4924 movies_found = []
4925 try:
4926 movies_found = imdb_access.search_movie(moviename.encode("ascii", 'ignore'))
4927 except Exception:
4928 pass
4929 tmp_movies={}
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]):
4934 continue
4935 tmp_movies[temp.keys()[0]] = temp[temp.keys()[0]]
4936 for year in years:
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
4942 break
4943 if extradata[u'inetref']:
4944 break
4945 return extradata
4946 # end _getExtraMiroDetails()
4947
4948 def updateMiroVideo(self, program):
4949 '''Update the information in a Miro/MythVideo record
4950 return nothing
4951 '''
4952 global localhostname, graphicsDirectories
4953
4954 mirodetails = program[u'miro']
4955
4956 for intid in mirodetails[u'intid']:
4957 changed_fields = {}
4958 for key in graphicsDirectories.keys():
4959 if key == u'screenshot':
4960 continue
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]
4964
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)
4972 try:
4973 dir_list = os.listdir(unicode(dirName, 'utf8'))
4974 except (UnicodeEncodeError, TypeError):
4975 dir_list = os.listdir(dirName)
4976 index = 1
4977 while index != 0:
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()):
4982 break
4983 else:
4984 changed_fields[u'title'] = filename
4985 if self.config['simulation']:
4986 sys.stdout.write(
4987 u"Simulation rename Miro-MythTV movie trailer from (%s) to (%s)\n" % (mirodetails[u'pathfilename'], fullfilename))
4988 else:
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):
4996 continue
4997 if changed_fields[key][0] == u'/':
4998 continue
4999 else:
5000 changed_fields.remove(key)
5001 break
5002 index+=1
5003
5004 if len(changed_fields):
5005 if self.config['simulation']:
5006 if program['subtitle']:
5007 sys.stdout.write(
5008 u"Simulation MythTV DB update for Miro video (%s - %s)\n" % (program['title'], program['subtitle']))
5009 else:
5010 sys.stdout.write(
5011 u"Simulation MythTV DB update for Miro video (%s)\n" % (program['title'],))
5012 else:
5013 Video(intid, db=mythvideo).update(changed_fields)
5014 # end updateMiroVideo()
5015
5016 def _getScheduledRecordedProgramList(self):
5017 '''Find all Scheduled and Recorded programs
5018 return array of found programs, if none then empty array is returned
5019 '''
5020 global localhostname
5021 programs=[]
5022
5023 # Get pending recordings
5024 try:
5025 progs = mythbeconn.getUpcomingRecordings()
5026 except MythError, e:
5027 sys.stderr.write(u"\n! Error: Getting Upcoming Recordings list: %s\n" % e.args[0])
5028 return programs
5029
5030 for prog in progs:
5031 record={}
5032 if prog.title == None:
5033 continue
5034 record['title'] = prog.title
5035 record['subtitle'] = prog.subtitle
5036 record['seriesid'] = prog.seriesid
5037
5038 if record['subtitle'] and prog.airdate != None:
5039 record['originalairdate'] = prog.airdate.year
5040 else:
5041 if prog.year != '0':
5042 record['originalairdate'] = prog.year
5043 elif prog.airdate != None:
5044 record['originalairdate'] = prog.airdate.year
5045 for program in programs: # Skip duplicates
5046 if program['title'] == record['title']:
5047 break
5048 else:
5049 programs.append(record)
5050
5051 # Get recorded records
5052 try:
5053 recordedlist = list(mythdb.searchRecorded(hostname=localhostname))
5054 except MythError, e:
5055 sys.stderr.write(u"\n! Error: Getting recorded programs list: %s\n" % e.args[0])
5056 return programs
5057
5058 if not recordedlist:
5059 return programs
5060
5061 recordedprogram = {}
5062 for recordedRecord in recordedlist:
5063 if recordedRecord.recgroup == u'Deleted':
5064 continue
5065 recorded = {}
5066 if recordedRecord.title == None:
5067 continue
5068 if recordedRecord.chanid == 9999:
5069 recorded[u'miro_tv'] = True
5070 recorded[u'title'] = recordedRecord.title
5071 recorded[u'subtitle'] = recordedRecord.subtitle
5072 recorded[u'seriesid'] = recordedRecord.seriesid
5073 for program in programs: # Skip duplicates
5074 if program['title'] == recorded['title']:
5075 break
5076 else:
5077 programs.append(recorded)
5078 # Get Release year for recorded movies
5079 # Get Recorded videos recordedprogram / airdate
5080 try:
5081 recordedDetails = dict(RecordedProgram.fromRecorded(recordedRecord))
5082 except MythError, e:
5083 sys.stderr.write(u"\n! Error: Getting recordedprogram table record: %s\n" % e.args[0])
5084 continue
5085 if not len(recordedDetails):
5086 continue
5087 if not recordedDetails['subtitle']:
5088 recordedprogram[recordedDetails['title']]= u'%d' % recordedDetails['airdate']
5089
5090 # Add release year to recorded movies
5091 for program in programs:
5092 if recordedprogram.has_key(program['title']):
5093 program['originalairdate'] = recordedprogram[program['title']]
5094
5095
5096 # Add real names to mb_tv if they are among the recorded videos
5097 if len(self.config['mb_tv_channels']):
5098 for program in programs:
5099 programtitle = filter(is_not_punct_char, program[u'title'].lower())
5100 if programtitle in self.config['mb_tv_channels'].keys():
5101 self.config['mb_tv_channels'][programtitle][1] = program[u'title']
5102
5103 # Check that each program has an original airdate
5104 for program in programs:
5105 if not program.has_key('originalairdate'):
5106 program['originalairdate'] = u'0000' # Set the original airdate to zero (unknown)
5107
5108 # If there are any Miro TV or movies to process then add them to the list
5109 if len(self.config['mb_tv_channels']) or len(self.config['mb_movies']):
5110 miromythvideorecs = self._getMiroVideometadataRecords()
5111 if miromythvideorecs:
5112 # Create array used to check for duplicates
5113 duplicatekeys = {}
5114 i = 0
5115 for program in programs:
5116 programtitle = filter(is_not_punct_char, program[u'title'].lower())
5117 if programtitle in self.config['mb_tv_channels'].keys():
5118 if not program[u'title'] in duplicatekeys:
5119 duplicatekeys[program[u'title']] = i
5120 elif programtitle in self.config['mb_movies'].keys():
5121 moviename = program['subtitle']
5122 if not moviename:
5123 moviename = ''
5124 else:
5125 index = moviename.find(self.config['mb_movies'][programtitle])
5126 if index != -1:
5127 moviename = moviename[:index].strip()
5128 if not moviename in duplicatekeys:
5129 duplicatekeys[moviename] = i
5130 i+=1
5131
5132 for record in miromythvideorecs:
5133 program = {}
5134 program[u'title'] = record[u'title']
5135 program[u'subtitle'] = record[u'subtitle']
5136 program[u'originalairdate'] = record[u'year']
5137 recordtitle = filter(is_not_punct_char, record[u'title'].lower())
5138 if recordtitle in self.config['mb_tv_channels'].keys():
5139 if not record[u'title'] in duplicatekeys.keys():
5140 program[u'miro'] = self._getExtraMiroDetails(record, u'tv')
5141 duplicatekeys[program[u'title']] = len(programs)
5142 programs.append(program)
5143 self.config['mb_tv_channels'][recordtitle][1] = record[u'title']
5144 elif programs[duplicatekeys[program[u'title']]].has_key(u'miro'):
5145 programs[duplicatekeys[program[u'title']]][u'miro'][u'intid'].append(record[u'intid'])
5146 else:
5147 programs[duplicatekeys[program[u'title']]][u'miro'] = self._getExtraMiroDetails(record, u'tv')
5148 elif recordtitle in self.config['mb_movies'].keys():
5149 moviename = record['subtitle']
5150 if not moviename:
5151 moviename = ''
5152 else:
5153 index = moviename.find(self.config['mb_movies'][filter(is_not_punct_char, program[u'title'].lower())])
5154 if index != -1:
5155 moviename = moviename[:index].strip()
5156 if not moviename in duplicatekeys.keys():
5157 program[u'miro'] = self._getExtraMiroDetails(record, u'movies')
5158 if program[u'miro'][u'inetref']:
5159 duplicatekeys[moviename] = len(programs)
5160 programs.append(program)
5161 elif programs[duplicatekeys[moviename]].has_key(u'miro'):
5162 programs[duplicatekeys[moviename]][u'miro'][u'intid'].append(record[u'intid'])
5163 else:
5164 program[u'miro'] = self._getExtraMiroDetails(record, u'movies')
5165 if program[u'miro'][u'inetref']:
5166 programs[duplicatekeys[moviename]][u'miro'] = self._getExtraMiroDetails(record, u'movies')
5167
5168 # Check that each program has seriesid
5169 for program in programs:
5170 if not program.has_key('seriesid'):
5171 program['seriesid'] = u'' # Set an empty seriesid - Generall only for Miro Videos
5172 if program['seriesid'] == None:
5173 program['seriesid'] = u'' # Set an empty seriesid
5174
5175 return programs
5176 # end _getScheduledRecordedProgramList
5177
5178
5179 def _getScheduledRecordedTVGraphics(self, program, graphics_type):
5180 '''Get TV show graphics for Scheduled and Recorded TV programs
5181 return None if no graphics found
5182 return fullpath and filename of downloaded graphics file
5183 '''
5184 if graphics_type == 'coverfile':
5185 graphics_type = 'poster'
5186
5187 self.config['sid'] = None
5188 if self.config['series_name_override']:
5189 if self.config['series_name_override'].has_key(program['title'].lower()):
5190 self.config['sid'] = self.config['series_name_override'][program['title'].lower()]
5191 # Find out if there are any Series level graphics available
5192 self.config['toprated'] = True
5193 self.config['episode_name'] = None
5194 self.config['series_name'] = program['title']
5195 self.config['season_num'] = None
5196 self.config['episode_num'] = None
5197
5198 series_graphics = self.getGraphics(graphics_type)
5199
5200 if series_graphics != None:
5201 cfile = { 'file_seriesname': program['title'],
5202 'inetref': self.config['sid'],
5203 'seasno': self.config['season_num'],
5204 'epno': self.config['episode_num'],
5205 'filepath':u'',
5206 'filename': program['title'],
5207 'ext':u'',
5208 'categories':u''
5209 }
5210 return self._getTvdbGraphics(cfile, graphics_type, toprated=True, watched=True)
5211 return None
5212 # end _getScheduledRecordedTVGraphics
5213
5214 def _downloadScheduledRecordedGraphics(self):
5215 '''Get Scheduled and Recorded programs and Miro vidoes get their graphics if not already
5216 downloaded
5217 return (nothing is returned)
5218 '''
5219 global localhostname
5220
5221 # Initialize reporting stats
5222 total_progs_checked = 0
5223 total_posters_found = 0
5224 total_banners_found = 0
5225 total_fanart_found = 0
5226 total_posters_downloaded = 0
5227 total_banners_downloaded = 0
5228 total_fanart_downloaded = 0
5229 total_miro_tv = 0
5230 total_miro_movies = 0
5231
5232 programs = self._getScheduledRecordedProgramList()
5233
5234 if not len(programs): # Is there any programs to process?
5235 return
5236
5237 # Add any Miro Bridge mb_tv dictionary items to 'series_name_override' dictionary
5238 if not self.config['series_name_override'] and len(self.config['mb_tv_channels']):
5239 self.config['series_name_override'] = {}
5240 for miro_tv_key in self.config['mb_tv_channels'].keys():
5241 if self.config['mb_tv_channels'][miro_tv_key][0]:
5242 self.config['series_name_override'][self.config['mb_tv_channels'][miro_tv_key][1].lower()] = self.config['mb_tv_channels'][miro_tv_key][0]
5243
5244 total_progs_checked = len(programs)
5245
5246 # Get totals of Miro TV shows and movies that will be processed
5247 for program in programs:
5248 if program.has_key(u'miro'):
5249 if not program[u'miro'][u'tv']:
5250 total_miro_movies+=1
5251 else:
5252 total_miro_tv+=1
5253 elif program.has_key(u'miro_tv'):
5254 if filter(is_not_punct_char, program[u'title'].lower()) in self.config['mb_movies'].keys():
5255 total_miro_movies+=1
5256 else:
5257 total_miro_tv+=1
5258
5259 # Prossess all TV shows and Movies
5260 for program in programs:
5261 program['need'] = False # Initalize that this program does not need graphic(s) downloaded
5262 mirodetails = None
5263 program_override_tv = False
5264 # Check if a subtitle-less program is really a TV show with an override. This compensates for
5265 # poor EPG data sources (as has been reported from at least Australia)
5266 if not program['subtitle'] and program['title'].lower() in self.config['series_name_override']:
5267 try:
5268 result = self._searchforSeries(program['title'])
5269 program_override_tv = True
5270 except Exception, e:
5271 pass
5272
5273 # Even movies get the ' Season' added to the image names so that movie such as '1408' do not clash
5274 # with TMDB#ed image names
5275 pattern = u'%s Season*.*'
5276 if not program.has_key(u'miro'):
5277 if program['subtitle'] or program_override_tv:
5278 graphics_name = program['title']
5279 else:
5280 if not int(program['originalairdate']):
5281 graphics_name = program['title']
5282 else:
5283 graphics_name = "%s (%s)" % (program['title'], program['originalairdate'])
5284 else:
5285 mirodetails = program[u'miro']
5286 if mirodetails[u'tv']:
5287 graphics_name = program['title']
5288 else:
5289 graphics_name = mirodetails[u'inetref']
5290
5291 self.absolutepath = False # All Scheduled Recorded and Miro videos start in the SG "Default"
5292
5293 # Search for graphics that are already downloaded
5294 for directory in graphicsDirectories.keys():
5295 if directory == 'screenshot': # There is no downloading of screenshots required
5296 program[directory] = True
5297 continue
5298 if directory == 'banner' and not program['subtitle']: # No banners for movies
5299 program[directory] = True
5300 continue
5301 elif mirodetails:
5302 if not mirodetails[u'tv'] and directory == 'banner': # No banners for movies
5303 program[directory] = True
5304 continue
5305 if not mirodetails:
5306 filename = program['title']
5307 elif mirodetails[u'tv']:
5308 filename = program['title']
5309 else:
5310 filename = mirodetails[u'inetref']
5311
5312 # Deal with TV series names that would generate invalid file names for images TV and movies
5313 self.program_seriesid = None
5314 if not isValidPosixFilename(filename) and program['seriesid'] != u'':
5315 filename = program['seriesid']
5316 self.program_seriesid = program['seriesid']
5317
5318 # Actual check for existing graphics
5319 for dirct in self.config[graphicsDirectories[directory]]:
5320 try:
5321 dir_list = os.listdir(unicode(dirct, 'utf8'))
5322 except (UnicodeEncodeError, TypeError):
5323 dir_list = os.listdir(dirct)
5324 for flenme in dir_list:
5325 if fnmatch.fnmatch(flenme.lower(), (pattern % filename).lower()):
5326 program[directory] = True
5327 if directory == 'coverfile':
5328 total_posters_found +=1
5329 elif directory == 'banner':
5330 total_banners_found +=1
5331 else:
5332 total_fanart_found +=1
5333 if mirodetails: # Update the Miro MythVideo records with any existing graphics
5334 mirodetails[directory] = self.rtnRelativePath(u'%s/%s' % (dirct, flenme), directory)
5335 break
5336 else:
5337 continue
5338 break
5339 else:
5340 program['need'] = True
5341 program[directory] = False
5342
5343 # Check if there are any graphics to download
5344 if not program['need']:
5345 if not mirodetails:
5346 filename = program['title']
5347 elif mirodetails[u'tv']:
5348 filename = program['title']
5349 else:
5350 filename = mirodetails[u'moviename']
5351 self._displayMessage("All Graphics already downloaded for [%s]" % filename)
5352 if mirodetails: # Update the Miro MythVideo records with any new graphics
5353 self.updateMiroVideo(program)
5354 continue
5355
5356 if not mirodetails:
5357 # It is more efficient to find inetref of movie once
5358 if not program['subtitle'] and not program_override_tv:
5359 if not program.has_key('inetref'): # Was the inetref number already found?
5360 inetref = self._getTmdbIMDB(graphics_name, watched=True)
5361 if not inetref:
5362 self._displayMessage("No movie inetref [%s]" % graphics_name)
5363 # Fake subtitle as this may be a TV series without a subtitle
5364 program['subtitle']=' '
5365 else:
5366 self._displayMessage("Found movie inetref (%s),[%s]" % (inetref, graphics_name))
5367 program['inetref'] = inetref
5368 elif not mirodetails[u'tv']:
5369 program['inetref'] = mirodetails[u'inetref']
5370
5371 # Download missing graphics
5372 for key in graphicsDirectories.keys():
5373 if program[key]: # Check if this type of graphic is already downloaded
5374 continue
5375 miromovieflag = False
5376 if mirodetails:
5377 if not mirodetails[u'tv']:
5378 miromovieflag = True
5379 # This is a TV episode or Miro TV show
5380 if (program['subtitle'] or program_override_tv) and not miromovieflag:
5381 results = self._getScheduledRecordedTVGraphics(program, key)
5382 if results:
5383 if not mirodetails:
5384 filename = program['title']
5385 elif mirodetails[u'tv']:
5386 filename = program['title']
5387 else:
5388 filename = mirodetails[u'moviename']
5389 if key == 'coverfile':
5390 total_posters_downloaded +=1
5391 elif key == 'banner':
5392 total_banners_downloaded +=1
5393 elif key == 'fanart':
5394 total_fanart_downloaded +=1
5395 if mirodetails: # Save the filename for storing later
5396 mirodetails[key] = results
5397 else:
5398 self._displayMessage("TV Series - No (%s) for [%s]" % (key, program['title']))
5399 else: # This is a movie
5400 title = program['title']
5401 filename = program['title']
5402 if miromovieflag:
5403 title = mirodetails[u'inetref']
5404 filename = mirodetails[u'inetref']
5405 cfile = { 'file_seriesname': title,
5406 'inetref': program['inetref'],
5407 'seasno': 0,
5408 'epno': 0,
5409 'filepath':u'',
5410 'filename': filename,
5411 'ext':u'',
5412 'categories':u''
5413 }
5414 if key == 'coverfile':
5415 g_type = '-P'
5416 else:
5417 g_type = '-B'
5418 results = self._getTmdbGraphics(cfile, g_type, watched=True)
5419 if not results:
5420 results = self._getSecondarySourceGraphics(cfile, key, watched=True)
5421 if results:
5422 if key == 'coverfile':
5423 total_posters_downloaded +=1
5424 elif key == 'fanart':
5425 total_fanart_downloaded +=1
5426 if mirodetails: # Save the filename for storing later
5427 mirodetails[key] = results
5428 else:
5429 if not mirodetails:
5430 filename = program['title']
5431 else:
5432 filename = mirodetails[u'moviename']
5433 self._displayMessage("No (%s) for [%s]" % (key, filename))
5434
5435 if mirodetails: # Update the Miro MythVideo records with any new graphics
5436 self.updateMiroVideo(program)
5437
5438 # Print statistics
5439 sys.stdout.write(u'\n-----Scheduled & Recorded Statistics-------\nNumber of Scheduled & Recorded ......(% 5d)\nNumber of Fanart graphics found .....(% 5d)\nNumber of Poster graphics found .....(% 5d)\nNumber of Banner graphics found .....(% 5d)\nNumber of Fanart graphics downloaded (% 5d)\nNumber of Poster graphics downloaded (% 5d)\nNumber of Banner graphics downloaded (% 5d)\nNumber of Miro TV Shows ............ (% 5d)\nNumber of Miro Movie Trailers ...... (% 5d)\n' % (total_progs_checked, total_fanart_found, total_posters_found, total_banners_found, total_fanart_downloaded, total_posters_downloaded, total_banners_downloaded, total_miro_tv, total_miro_movies))
5440
5441 if len(programs):
5442 sys.stdout.write(u'\n-------------Scheduled & Recorded----------\n')
5443 for program in programs:
5444 if not program.has_key(u'miro'):
5445 if program.has_key(u'miro_tv'):
5446 if filter(is_not_punct_char, program[u'title'].lower()) in self.config['mb_movies'].keys():
5447 sys.stdout.write(u'Miro Movie Trailer: %s\n' % (program['title'], ))
5448 else:
5449 sys.stdout.write(u'Miro TV Show: %s\n' % (program['title'], ))
5450 else:
5451 if program['subtitle']:
5452 sys.stdout.write(u'%s\n' % (program['title'], ))
5453 else:
5454 if program['originalairdate'] != u'0000':
5455 sys.stdout.write(u'%s\n' % ("%s (%s)" % (program['title'], program['originalairdate'])))
5456 else:
5457 sys.stdout.write(u'%s\n' % (program['title'], ))
5458 elif program[u'miro'][u'tv']:
5459 sys.stdout.write(u'Miro TV Show: %s\n' % (program['title'], ))
5460 else:
5461 sys.stdout.write(u'Miro Movie Trailer: %s\n' % (program[u'miro'][u'moviename'], ))
5462 return
5463 # end _downloadScheduledRecordedGraphics()
5464
5465
5466 def findFileInDir(self, filename, directories, suffix=None, fuzzy_match=False):
5467 '''Find if a file is in any of the specified directories. An exact match or a variation.
5468 return False - File not found in directories
5469 return True - Absolute file name and path
5470 '''
5471 (dirName, fileName) = os.path.split(filename)
5472 (fileBaseName, fileExtension) = os.path.splitext(fileName)
5473 if fuzzy_match: # Match even when the names are not exactly the same by removing punctuation
5474 for dirct in directories:
5475 try:
5476 dir_list = os.listdir(unicode(dirct, 'utf8'))
5477 except (UnicodeEncodeError, TypeError):
5478 dir_list = os.listdir(dirct)
5479 match_list = []
5480 for file_name in dir_list:
5481 match_list.append(filter(is_not_punct_char, file_name.lower()))
5482 if suffix:
5483 if fileBaseName.find(suffix) == -1:
5484 file_path = filter(is_not_punct_char, (u"%s%s%s" % (fileBaseName, suffix, fileExtension)).lower())
5485 file_path2 = filter(is_not_punct_char, (u"%s%s" % (fileBaseName, fileExtension)).lower())
5486 else:
5487 file_path = filter(is_not_punct_char, (u"%s%s" % (fileBaseName, fileExtension)).lower())
5488 file_path2 = filter(is_not_punct_char, (u"%s%s" % (fileBaseName.replace(suffix, u''), fileExtension)).lower())
5489 if file_path in match_list:
5490 return u'%s/%s' % (dirct, dir_list[match_list.index(file_path)])
5491 if file_path2 in match_list:
5492 return u'%s/%s' % (dirct, dir_list[match_list.index(file_path2)])
5493 continue
5494 else:
5495 file_path = filter(is_not_punct_char, (u"%s%s" % (fileBaseName, fileExtension)).lower())
5496 if file_path in match_list:
5497 return u'%s/%s' % (dirct, dir_list[match_list.index(file_path)])
5498 else:
5499 return False
5500 else: # Find an exact match
5501 for directory in directories:
5502 if filename[0] != u'/' and dirName != u'':
5503 dir_name = u"%s/%s" % (directory, dirName)
5504 else:
5505 dir_name = directory
5506 if suffix:
5507 if fileBaseName.find(suffix) == -1:
5508 file_path = u"%s/%s%s%s" % (dir_name, fileBaseName, suffix, fileExtension)
5509 file_path2 = u'%s/%s' % (dir_name, fileName)
5510 else:
5511 file_path = u'%s/%s' % (dir_name, fileName)
5512 file_path2 = u'%s/%s' % (dir_name, fileName.replace(suffix, u''))
5513 if os.path.isfile(file_path):
5514 return file_path
5515 if os.path.isfile(file_path2):
5516 return file_path2
5517 continue
5518 else:
5519 file_path = u'%s/%s' % (dir_name, fileName)
5520 if os.path.isfile(file_path):
5521 return file_path
5522 else:
5523 return False
5524 # end findFileInDir()
5525
5526
5527 # Local Variables
5528 num_secondary_source_graphics_downloaded=0
5529 num_secondary_source_metadata_downloaded=0
5530
5531 def processMythTvMetaData(self):
5532 '''Check each video file in the mythvideo directories download graphics files and meta data then
5533 update MythTV data base meta data with any new information.
5534 '''
5535 # Verify that the proper fields are present
5536 db_version = mythdb.settings.NULL.DBSchemaVer
5537 field_names = mythvideo.tablefields['videometadata']
5538 for field in ['season', 'episode', 'coverfile', 'screenshot', 'banner', 'fanart']:
5539 if not field in field_names:
5540 sys.stderr.write(u"\n! Error: Your MythTv data base scheme version (%s) does not have the necessary fields at least (%s) is missing\n\n" % (db_version, field))
5541 sys.exit(1)
5542
5543 # Initailize and instance to the TMDB api
5544 apikey = "c27cb71cff5bd76e1a7a009380562c62"
5545 if self.config['interactive']:
5546 # themoviedb.org api key given by Travis Bell for Mythtv
5547 self.config['tmdb_api'] = tmdb_api.MovieDb(apikey,
5548 mythtv = True,
5549 interactive = True,
5550 select_first = False,
5551 debug = self.config['debug_enabled'],
5552 custom_ui = None,
5553 language = self.config['local_language'],
5554 search_all_languages = True,)
5555 else:
5556 self.config['tmdb_api'] = tmdb_api.MovieDb(apikey,
5557 mythtv = True,
5558 interactive = False,
5559 select_first = False,
5560 debug = self.config['debug_enabled'],
5561 language = self.config['local_language'],
5562 search_all_languages = True,)
5563
5564 # If there were directories specified move them and update the MythTV db meta data accordingly
5565 if self.config['video_dir']:
5566 if len(self.config['video_dir']) % 2 == 0:
5567 validFiles = self._moveVideoFiles(self.config['video_dir'])
5568 self.config[u'file_move_flag'] = False
5569 else:
5570 sys.stderr.write(u"\n! Error: When specifying target (file or directory) to move to a destination (directory) they must always be in pairs (target and destination directory).\nYou specified an uneven number of variables (%d) for target and destination pairs.\nVariable count (%s)\n" % (len(self.config['video_dir']), self.config['video_dir']))
5571 sys.exit(1)
5572
5573 # Check if only missing inetref video's should be processed
5574 if self.config['mythtv_inetref'] or self.config['mythtv_ref_num']:
5575 validFiles = self._findMissingInetref()
5576 if validFiles == None:
5577 sys.stderr.write(u"\n! Warning: There were no missing interef video files found.\n\n")
5578 sys.exit(0)
5579 elif not len(validFiles):
5580 sys.stderr.write(u"\n! Warning: There were no missing interef video files found.\n\n")
5581 sys.exit(0)
5582
5583 # Check if this is a Scheduled and Recorded graphics download request
5584 if self.config['mythtv_watched']:
5585 self._downloadScheduledRecordedGraphics()
5586 sys.exit(0)
5587
5588 # Check if this is just a Janitor (clean up unused graphics files) request
5589 if self.config['mythtvjanitor']:
5590 self._graphicsCleanup()
5591 sys.exit(0)
5592
5593 directories=self.config['mythvideo']
5594
5595 if not len(directories):
5596 sys.stderr.write(u"\n! Error: There must be a video directory specified in MythTv\n")
5597 sys.exit(1)
5598
5599 # Set statistics
5600 num_processed=0
5601 num_fanart_downloads=0
5602 num_posters_downloads=0
5603 num_banners_downloads=0
5604 num_episode_metadata_downloads=0
5605 num_movies_using_imdb_numbers=0
5606 num_symlinks_created=0
5607 num_mythdb_updates=0
5608 num_posters_below_min_size=0
5609 videos_with_small_posters=[]
5610 videos_using_imdb_numbers=[]
5611 videos_updated_metadata=[]
5612 missing_inetref=[]
5613
5614 sys.stdout.write(u'Mythtv video database maintenance start: %s\n' % (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M"))
5615
5616 if not self.config['video_dir'] and not self.config['mythtv_inetref'] and not self.config['mythtv_ref_num']:
5617 allFiles = self._findFiles(directories, self.config['recursive'] , verbose = self.config['debug_enabled'])
5618 validFiles = self._processNames(allFiles, verbose = self.config['debug_enabled'], movies=True)
5619
5620 if not len(validFiles):
5621 sys.stderr.write(u"\n! Error: No valid video files found\n")
5622 sys.exit(1)
5623
5624 tv_series_season_format=u"%s/%s Season %d.%s"
5625 tv_series_format=u"%s/%s.%s"
5626 for cfile in validFiles:
5627 self._displayMessage(u"\nNow processing video file (%s)(%s)(%s)\n" % (cfile['filename'], cfile['seasno'], cfile['epno']))
5628 num_processed+=1
5629
5630 videopath = tv_series_format % (cfile['filepath'], cfile['filename'], cfile['ext'])
5631 # Find the MythTV meta data
5632 result = mythvideo.getVideo(exactfile=videopath)
5633 if result == None:
5634 intid = result
5635 else:
5636 intid = result.intid
5637 if not intid:
5638 result = mythvideo.getVideo(exactfile=self.rtnRelativePath(videopath, u'mythvideo'), host=localhostname.lower())
5639 if result == None:
5640 intid = result
5641 has_metadata = False
5642 else:
5643 intid = result.intid
5644 if result.category == 'none' and result.year == 1895:
5645 has_metadata = False
5646 else:
5647 has_metadata = True
5648 else:
5649 if result.category == 'none' and result.year == 1895:
5650 has_metadata = False
5651 else:
5652 has_metadata = True
5653
5654 if intid == None:
5655 # Unless explicitly requested with options -MI or -MG do not add missing videos to DB
5656 if not self.config['interactive'] and not self.config['mythtv_guess']:
5657 continue
5658 # Create a new empty entry
5659 sys.stdout.write(u"\n\nEntry does not exist in MythDB. Adding (%s).\n" % cfile['filename'])
5660 new_rec = {'title': cfile['file_seriesname'], 'filename': self.rtnRelativePath(videopath, u'mythvideo')}
5661 videopath = self.rtnRelativePath(videopath, u'mythvideo')
5662 if videopath[0] == '/':
5663 intid = Video(db=mythvideo).create(new_rec).intid
5664 else:
5665 new_rec['host'] = localhostname.lower()
5666 intid = Video(db=mythvideo).create(new_rec).intid
5667 elif not has_metadata:
5668 sys.stdout.write(u"\n\nEntry exists in MythDB but category is 0 and year is 1895 (default values).\nUpdating (%s).\n" % cfile['filename'])
5669 filename = self.rtnRelativePath(videopath, u'mythvideo')
5670 if filename[0] == u'/':
5671 Video(intid, db=mythvideo).update({'filename': filename, u'host': u''})
5672 else:
5673 Video(intid, db=mythvideo).update({'filename': filename, u'host': localhostname.lower()})
5674 if cfile['seasno'] == 0 and cfile['epno'] == 0:
5675 movie=True
5676 else:
5677 movie=False
5678
5679 # Get a dictionary of the existing meta data plus a copy for update comparison
5680 meta_dict={}
5681 vim = Video(intid, db=mythvideo)
5682 for key in vim.keys():
5683 meta_dict[key] = vim[key]
5684
5685 # Fix a metadata record that has an incorrectly initialized inetref number value
5686 if meta_dict['inetref'] == None:
5687 meta_dict['inetref'] = u'00000000'
5688 available_metadata = dict(meta_dict)
5689
5690 available_metadata['season']=cfile['seasno']
5691 available_metadata['episode']=cfile['epno']
5692
5693 if available_metadata['title'] == u'':
5694 available_metadata['title'] = cfile['file_seriesname']
5695
5696 # Set whether a video file is stored in a Storage Group or not
5697 if available_metadata['filename'][0] == u'/':
5698 self.absolutepath = True
5699 else:
5700 self.absolutepath = False
5701
5702 # There must be an Internet reference number. Get one for new records.
5703 if _can_int(meta_dict['inetref']) and not meta_dict['inetref'] == u'00000000' and not meta_dict['inetref'] == '':
5704 if meta_dict['inetref'] == '99999999': # Records that are not updated by Jamu
5705 continue
5706 inetref = meta_dict['inetref']
5707 cfile['inetref'] = meta_dict['inetref']
5708 else:
5709 if movie:
5710 if not self.config['interactive'] and not self.config['mythtv_guess']:
5711 sys.stderr.write(u'\n! Warning: Skipping "%s" as there is no TMDB or IMDB number for this movie.\nUse interactive option (-I) or (-R) to select the TMDB or IMDB number.\n\n' % (cfile['file_seriesname']))
5712 continue
5713 inetref = self._getTmdbIMDB(available_metadata['title'])
5714 cfile['inetref'] = inetref
5715 if not inetref:
5716 self._displayMessage(u"themoviedb.com does not recognize the movie (%s) - Cannot update metadata - skipping\n" % available_metadata['title'])
5717 missing_inetref.append(available_metadata['title'])
5718 continue
5719 # Only update the reference number
5720 if self.config['mythtv_ref_num'] or inetref == '99999999':
5721 Video(intid, db=mythvideo).update({'inetref': inetref})
5722 num_mythdb_updates+=1
5723 videos_updated_metadata.append(cfile['filename'])
5724 self._displayMessage(u"\nReference number (%s) added for (%s) \n" % (inetref, cfile['filename']))
5725 continue
5726 else:
5727 copy = {}
5728 for key in available_metadata.keys():
5729 copy[key] = available_metadata[key]
5730 tmp_dict = self._getTvdbMetadata(cfile, copy)
5731 if not tmp_dict:
5732 self._displayMessage(u"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile['seasno'], cfile['epno'], videopath))
5733 missing_inetref.append(available_metadata['title'])
5734 continue
5735 inetref = tmp_dict['inetref']
5736 available_metadata['title'] = tmp_dict['title']
5737 cfile['file_seriesname'] = tmp_dict['title']
5738 # Only update the reference number and title
5739 if self.config['mythtv_ref_num'] or inetref == '99999999':
5740 if inetref == u'99999999':
5741 Video(intid, db=mythvideo).update({'inetref': inetref})
5742 else:
5743 Video(intid, db=mythvideo).update({'inetref': inetref, 'title': tmp_dict['title']})
5744 num_mythdb_updates+=1
5745 videos_updated_metadata.append(cfile['filename'])
5746 self._displayMessage(u"\nReference number (%s) added for (%s) \n" % (inetref, cfile['filename']))
5747 continue
5748 cfile['inetref'] = inetref
5749 available_metadata['inetref'] = inetref
5750
5751 if (meta_dict['subtitle'] == None or meta_dict['subtitle'] == '') and not movie:
5752 tmp_subtitle = self._getSubtitle(cfile)
5753 if tmp_subtitle == None:
5754 self._displayMessage(u"thetvdb.com does not recognize the Season(%d) Episode(%d) for video file(%s) - skipping\n" % (cfile['seasno'], cfile['epno'], videopath))
5755 continue
5756 else:
5757 available_metadata['subtitle'] = tmp_subtitle
5758 available_metadata['title'] = self.config['series_name']
5759 cfile['file_seriesname'] = self.config['series_name']
5760
5761 # Check if current inetref is a IMDB#
5762 # If so then check it can be changed to tmdb#
5763 # If it can be changed then rename any graphics and update meta data
5764 if movie and len(inetref) == 7:
5765 self._displayMessage(u"%s has IMDB# (%s)" % (available_metadata['title'], inetref))
5766 num_movies_using_imdb_numbers+=1
5767 videos_using_imdb_numbers.append(u"%s has IMDB# (%s)" % (available_metadata['title'], inetref))
5768 movie_data = self._getTmdbMetadata(cfile, dict(available_metadata))
5769 if movie_data.has_key('inetref'):
5770 if available_metadata['inetref'] != movie_data['inetref']:
5771 available_metadata['inetref'] = movie_data['inetref']
5772 inetref = movie_data['inetref']
5773 cfile['inetref'] = movie_data['inetref']
5774 for graphic_type in ['coverfile', 'banner', 'fanart']: # Rename graphics files
5775 if available_metadata[graphic_type] == None or available_metadata[graphic_type] == '':
5776 continue
5777 graphic_file = self.rtnAbsolutePath(available_metadata[graphic_type], graphicsDirectories[graphic_type])
5778 if os.path.isfile(graphic_file):
5779 filepath, filename = os.path.split(graphic_file)
5780 filename, ext = os.path.splitext( filename )
5781 ext = ext[1:]
5782 if self.config['simulation']:
5783 sys.stdout.write(
5784 u"Simulation renaming (%s) to (%s)\n" % (graphic_file, tv_series_format % (filepath, inetref+self.graphic_suffix[graphic_type], ext))
5785 )
5786 else:
5787 dest = tv_series_format % (filepath, inetref+self.graphic_suffix[graphic_type], ext)
5788 try:
5789 if not os.path.isfile(dest):
5790 os.rename(graphic_file, dest)
5791 except IOError, e:
5792 sys.stderr.write(
5793 u"Renaming image file (%s) to (%s) failed, error(%s)\n" % (graphic_file, dest, e))
5794
5795 self._displayMessage(u"Renamed (%s) to (%s)\n" % (graphic_file, tv_series_format % (filepath, inetref+self.graphic_suffix[graphic_type], ext)))
5796 available_metadata[graphic_type]= self.rtnRelativePath(dest, graphicsDirectories[graphic_type])
5797
5798 ###############################################################################
5799 # START of metadata Graphics logic - Checking, downloading, renaming
5800 ###############################################################################
5801 for graphic_type in ['coverfile', 'banner', 'fanart']:
5802 ###############################################################################
5803 # START of MOVIE graphics updating
5804 ###############################################################################
5805 # Check that there are local graphics path for abs path video
5806 # An abs path video can only use the FE specified graphic directories
5807 if self.absolutepath:
5808 if not len(self.config['localpaths'][graphicsDirectories[graphic_type]]):
5809 continue
5810 graphicsdirs = self.config['localpaths'][graphicsDirectories[graphic_type]]
5811 else:
5812 graphicsdirs = self.config[graphicsDirectories[graphic_type]]
5813 if movie:
5814 if graphic_type == 'banner':
5815 continue
5816 if graphic_type == 'coverfile':
5817 g_type = '-P'
5818 else:
5819 g_type = '-B'
5820 need_graphic = True
5821 undersized_graphic = False
5822 for ext in self.image_extensions:
5823 for graphicsdir in graphicsdirs:
5824 filename = self.findFileInDir(u"%s.%s" % (inetref, ext), [graphicsdir], suffix=self.graphic_suffix[graphic_type])
5825
5826 if filename:
5827 available_metadata[graphic_type]=self.rtnRelativePath(filename, graphicsDirectories[graphic_type])
5828 if graphic_type == 'coverfile':
5829 try:
5830 (width, height) = self.config['image_library'].open(filename).size
5831 if width < self.config['min_poster_size']:
5832 num_posters_below_min_size+=1
5833 videos_with_small_posters.append(cfile['filename'])
5834 undersized_graphic = True
5835 break
5836 except IOError:
5837 undersized_graphic = True
5838 break
5839 need_graphic = False
5840 break
5841 if not need_graphic:
5842 break
5843
5844 if need_graphic == True:
5845 dummy_graphic = self._getTmdbGraphics(cfile, g_type)
5846
5847 # Try secondary source if themoviedb.com did not have graphicrecord['title']
5848 if dummy_graphic == None or undersized_graphic == True:
5849 dummy_graphic = self._getSecondarySourceGraphics(cfile, graphic_type)
5850
5851 if dummy_graphic != None:
5852 available_metadata[graphic_type] = self.rtnRelativePath(dummy_graphic, graphicsDirectories[graphic_type])
5853 if graphic_type == 'fanart':
5854 self._displayMessage(u"Movie - Added fan art for(%s)" % cfile['filename'])
5855 num_fanart_downloads+=1
5856 else:
5857 self._displayMessage(u"Movie - Added a poster for(%s)" % cfile['filename'])
5858 num_posters_downloads+=1
5859 continue
5860 # END of Movie graphics updates ###############################################
5861 else:
5862 ###############################################################################
5863 # START of TV Series graphics updating
5864 ###############################################################################
5865 need_graphic = False
5866 new_format = True # Initialize that a graphics file NEEDS a new format
5867 # Check if an existing TV series graphic file is in the old naming format
5868 if available_metadata[graphic_type] != None and available_metadata[graphic_type] != 'No Cover' and available_metadata[graphic_type] != '':
5869 graphic_file = self.rtnAbsolutePath(available_metadata[graphic_type], graphicsDirectories[graphic_type])
5870 filepath, filename = os.path.split(graphic_file)
5871 filename, ext = os.path.splitext( filename )
5872 if filename.find(u' Season ') != -1:
5873 new_format = False
5874 else:
5875 need_graphic = True
5876 if need_graphic or new_format: # Graphic does not exist or is in an old format
5877 for ext in self.image_extensions:
5878 for graphicsdir in graphicsdirs:
5879 filename=self.findFileInDir(u"%s Season %d.%s" % (self.sanitiseFileName(available_metadata['title']), available_metadata['season'], ext), [graphicsdir], suffix=self.graphic_suffix[graphic_type], fuzzy_match=True)
5880 if filename:
5881 available_metadata[graphic_type]=self.rtnRelativePath(filename, graphicsDirectories[graphic_type])
5882 need_graphic = False
5883 if graphic_type == 'coverfile':
5884 try:
5885 (width, height) = self.config['image_library'].open(filename).size
5886 if width < self.config['min_poster_size']:
5887 num_posters_below_min_size+=1
5888 videos_with_small_posters.append(cfile['filename'])
5889 break
5890 except IOError:
5891 undersized_graphic = True
5892 break
5893 break
5894 if not need_graphic:
5895 break
5896 else:
5897 graphic_file = self.rtnAbsolutePath(available_metadata[graphic_type], graphicsDirectories[graphic_type])
5898 if not graphic_file == None:
5899 graphic_file = self.findFileInDir(graphic_file, [graphicsdir], suffix=self.graphic_suffix[graphic_type], fuzzy_match=True)
5900 if graphic_file == None:
5901 need_graphic = True
5902 if not need_graphic: # Have graphic but may be using an old naming convention
5903 must_rename = False
5904 season_missing = False
5905 suffix_missing = False
5906 if graphic_file.find(u' Season ') == -1: # Check for Season
5907 must_rename = True
5908 season_missing = True
5909 if graphic_file.find(self.graphic_suffix[graphic_type]) == -1:
5910 must_rename = True
5911 suffix_missing = True
5912 if must_rename:
5913 filepath, filename = os.path.split(graphic_file)
5914 baseFilename, ext = os.path.splitext( filename )
5915 baseFilename = self.sanitiseFileName(baseFilename)
5916 if season_missing and suffix_missing:
5917 newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename, available_metadata['season'], self.graphic_suffix[graphic_type], ext)
5918 elif suffix_missing:
5919 newFilename = u"%s/%s%s%s" % (filepath, baseFilename, self.graphic_suffix[graphic_type], ext)
5920 elif season_missing:
5921 baseFilename = baseFilename.replace(self.graphic_suffix[graphic_type], u'')
5922 newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename.replace(u' Season X', u''), available_metadata['season'], self.graphic_suffix[graphic_type], ext)
5923 if self.config['simulation']:
5924 sys.stdout.write(
5925 u"Simulation renaming (%s) to (%s)\n" % (graphic_file, newFilename)
5926 )
5927 else:
5928 os.rename(graphic_file, newFilename)
5929 available_metadata[graphic_type]= self.rtnRelativePath(newFilename, graphicsDirectories[graphic_type])
5930 else:
5931 available_metadata[graphic_type]= self.rtnRelativePath(graphic_file, graphicsDirectories[graphic_type])
5932 else: # Must see if a graphic is on thetvdb wiki
5933 if graphic_type == 'coverfile' or graphic_type == 'banner':
5934 available_metadata[graphic_type] = self.rtnRelativePath(self._getTvdbGraphics(cfile, graphic_type), graphicsDirectories[graphic_type])
5935 if available_metadata[graphic_type] == None:
5936 tmp = self._getTvdbGraphics(cfile, graphic_type, toprated=True)
5937 if tmp!= None:
5938 tmp_fullfilename = self.rtnAbsolutePath(tmp, graphicsDirectories[graphic_type])
5939 filepath, filename = os.path.split(tmp_fullfilename)
5940 baseFilename, ext = os.path.splitext( filename )
5941 baseFilename = self.sanitiseFileName(baseFilename)
5942 baseFilename = baseFilename.replace(self.graphic_suffix[graphic_type], u'')
5943 newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename.replace(u' Season X', u''), available_metadata['season'], self.graphic_suffix[graphic_type], ext)
5944 if self.config['simulation']:
5945 sys.stdout.write(
5946 u"Simulation rename (%s) to (%s)\n" % (tmp_fullfilename,newFilename)
5947 )
5948 else:
5949 self._displayMessage(u"Rename existing graphic %s for series (%s)" % (graphic_type, available_metadata['title']))
5950 try:
5951 os.rename(tmp_fullfilename, newFilename)
5952 if graphic_type == 'coverfile':
5953 self._displayMessage("1-Added a poster for(%s)" % cfile['filename'])
5954 num_posters_downloads+=1
5955 else:
5956 self._displayMessage("1-Added a banner for(%s)" % cfile['filename'])
5957 num_banners_downloads+=1
5958 available_metadata[graphic_type] = self.rtnRelativePath(newFilename, graphicsDirectories[graphic_type])
5959 except IOError, e:
5960 sys.stderr.write(
5961 u"IOError coping (%s) to (%s)\nError:(%s)\n" % (tmp_fullfilename, newFilename, e))
5962 else: # Try a secondary source
5963 dummy = self._getSecondarySourceGraphics(cfile, graphic_type)
5964 if dummy:
5965 if graphic_type == 'coverfile':
5966 self._displayMessage(u"1-Secondary source poster for(%s)" % cfile['filename'])
5967 num_posters_downloads+=1
5968 else:
5969 self._displayMessage(u"1-Secondary source banner for(%s)" % cfile['filename'])
5970 num_banners_downloads+=1
5971 available_metadata[graphic_type] = self.rtnRelativePath(dummy, graphicsDirectories[graphic_type])
5972 else: # download fanart
5973 tmp = self.rtnAbsolutePath(self._getTvdbGraphics(cfile, graphic_type, toprated=True), graphicsDirectories['fanart'])
5974 if tmp!= None:
5975 filepath, filename = os.path.split(tmp)
5976 baseFilename, ext = os.path.splitext( filename )
5977 baseFilename = self.sanitiseFileName(baseFilename)
5978 baseFilename = baseFilename.replace(self.graphic_suffix[graphic_type], u'')
5979 newFilename = u"%s/%s Season %d%s%s" % (filepath, baseFilename.replace(u' Season X', u''), available_metadata['season'], self.graphic_suffix[graphic_type], ext)
5980 if self.config['simulation']:
5981 sys.stdout.write(
5982 u"Simulation fanart rename (%s) to (%s)\n" % (tmp, newFilename)
5983 )
5984 else:
5985 try:
5986 os.rename(self.rtnAbsolutePath(tmp, graphicsDirectories[graphic_type]), newFilename)
5987 available_metadata['fanart'] = self.rtnRelativePath(newFilename, graphicsDirectories['fanart'])
5988 num_fanart_downloads+=1
5989 except IOError, e:
5990 sys.stderr.write(
5991 u"IOError coping (%s) to (%s)\nError:(%s)\n" % (self.rtnAbsolutePath(tmp, graphicsDirectories[graphic_type]), newFilename, e))
5992 else: # Try a secondary source
5993 dummy = self._getSecondarySourceGraphics(cfile, graphic_type)
5994 if dummy:
5995 available_metadata['fanart'] = self.rtnRelativePath(dummy, graphicsDirectories['fanart'])
5996 num_fanart_downloads+=1
5997 # END of TV Series graphics updating
5998 ###############################################################################
5999 # END of metadata Graphics logic - Checking, downloading, renaming
6000 ###############################################################################
6001
6002 ###############################################################################
6003 # START of metadata text logic - Checking, downloading, renaming
6004 ###############################################################################
6005 # Clean up meta data code
6006 if movie:
6007 if available_metadata['rating'] == 'TV Show':
6008 available_metadata['rating'] = 'NR'
6009
6010 # Check if any meta data needs updating
6011 metadata_update = True
6012 for key in available_metadata.keys():
6013 if key in self.config['metadata_exclude_as_update_trigger']:
6014 continue
6015 else:
6016 if key == 'rating' and (available_metadata[key] == 'NR' or available_metadata[key] == '' or available_metadata[key] == 'Unknown'):
6017 self._displayMessage(
6018 u"At least (%s) needs updating\n" % (key))
6019 break
6020 if key == 'userrating' and available_metadata[key] == 0.0:
6021 self._displayMessage(
6022 u"At least (%s) needs updating\n" % (key))
6023 break
6024 if key == 'length' and available_metadata[key] == 0:
6025 self._displayMessage(
6026 u"At least (%s) needs updating\n" % (key))
6027 break
6028 if key == 'category' and available_metadata[key] == 0:
6029 self._displayMessage(
6030 u"At least (%s) needs updating\n" % (key))
6031 break
6032 if key == 'year' and (available_metadata[key] == 0 or available_metadata[key] == 1895):
6033 self._displayMessage(
6034 u"At least (%s) needs updating\n" % (key))
6035 break
6036 if movie and key == 'subtitle': # There are no subtitles in movies
6037 continue
6038 if movie and key == 'plot' and available_metadata[key] != None:
6039 if len(available_metadata[key].split(' ')) < 10:
6040 self._displayMessage(
6041 u"The plot is less than 10 words check if a better plot exists\n")
6042 break
6043 if key == 'releasedate' and (available_metadata[key] == None or available_metadata[key] == date(1,1,1)):
6044 self._displayMessage(
6045 u"At least (%s) needs updating\n" % (key))
6046 break
6047 if key == 'hash' and (available_metadata['hash'] == u'' or available_metadata['hash'] == None):
6048 if (os.path.getsize(u'%s/%s.%s' % (cfile['filepath'], cfile['filename'], cfile['ext'])) < 65536 * 2):
6049 continue
6050 self._displayMessage(
6051 u"At least (%s) needs updating\n" % (key))
6052 break
6053 if available_metadata[key] == None or available_metadata[key] == '' or available_metadata[key] == 'None' or available_metadata[key] == 'Unknown':
6054 self._displayMessage(
6055 u"At least (%s) needs updating\n" % (key))
6056 break
6057 else:
6058 metadata_update = False
6059 if not movie and not len(available_metadata['inetref']) >= 5:
6060 self._displayMessage(
6061 u"At least (%s) needs updating\n" % ('inetref'))
6062 metadata_update = True
6063 # Find the video file's real duration in minutes
6064 try:
6065 length = self._getVideoLength(u'%s/%s.%s' % (cfile['filepath'], cfile['filename'], cfile['ext'], ))
6066 except:
6067 length = False
6068 if length:
6069 if length != available_metadata['length']:
6070 self._displayMessage(u"Video file real length (%d) minutes needs updating\n" % (length))
6071 metadata_update = True
6072
6073 # Fetch meta data
6074 genres_cast={'genres': u'', 'cast': u''}
6075 if metadata_update:
6076 copy = dict(available_metadata)
6077 if movie:
6078 tmp_dict = self._getTmdbMetadata(cfile, copy)
6079 else:
6080 tmp_dict = self._getTvdbMetadata(cfile, copy)
6081 num_episode_metadata_downloads+=1
6082 # Update meta data
6083 if tmp_dict:
6084 for key in ['genres', 'cast', 'countries']:
6085 if tmp_dict.has_key(key):
6086 genres_cast[key] = tmp_dict[key]
6087 for key in available_metadata.keys():
6088 if key in self.config['metadata_exclude_as_update_trigger']:
6089 continue
6090 else:
6091 if not tmp_dict.has_key(key):
6092 continue
6093 if key == 'userrating' and available_metadata[key] == 0.0:
6094 available_metadata[key] = tmp_dict[key]
6095 continue
6096 if key == 'length':
6097 try:
6098 length = self._getVideoLength(u'%s/%s.%s' %(cfile['filepath'], cfile['filename'], cfile['ext'], ))
6099 except:
6100 length = False
6101 if length:
6102 available_metadata['length'] = length
6103 else:
6104 available_metadata[key] = tmp_dict[key]
6105 continue
6106 available_metadata[key] = tmp_dict[key]
6107
6108 # Fix fields that must be prepared for insertion into data base
6109 available_metadata['title'] = self._make_db_ready(available_metadata['title'])
6110 available_metadata['director'] = self._make_db_ready(available_metadata['director'])
6111 available_metadata['plot'] = self._make_db_ready(available_metadata['plot'])
6112 if available_metadata['year'] == 0:
6113 available_metadata['year'] = 1895
6114 if available_metadata['coverfile'] == None:
6115 available_metadata['coverfile'] = u'No Cover'
6116 if len(genres_cast['genres']) and available_metadata['category'] == 'none':
6117 try:
6118 genres = genres_cast['genres'][:genres_cast['genres'].index(',')]
6119 except:
6120 genres = genres_cast['genres']
6121 available_metadata['category'] = genres
6122 self._displayMessage(u"Category added for (%s)(%s)" % (available_metadata['title'], available_metadata['category']))
6123
6124 # Make sure graphics relative/absolute paths are set PROPERLY based
6125 # on the 'filename' field being a relative or absolute path. A filename with an absolite path
6126 # CAN ONLY have graphics baed on absolute paths.
6127 # A filename with a relative path can have mixed absolute and relative path graphic files
6128 if available_metadata[u'filename'][0] == u'/':
6129 available_metadata[u'host'] = u''
6130 for key in [u'coverfile', u'banner', u'fanart']:
6131 if available_metadata[key] != None and available_metadata[key] != u'No Cover' and available_metadata[key] != u'':
6132 if available_metadata[key][0] != u'/':
6133 tmp = self.rtnAbsolutePath(available_metadata[key], graphicsDirectories[key])
6134 if tmp[0] != u'/':
6135 if key == u'coverfile':
6136 available_metadata[key] = u'No Cover'
6137 else:
6138 available_metadata[key] = u''
6139 else:
6140 available_metadata[u'host'] = localhostname.lower()
6141
6142 ###############################################################################
6143 # END of metadata text logic - Checking, downloading, renaming
6144 ###############################################################################
6145
6146 ###############################################################################
6147 # START of metadata updating the MythVideo record when graphics or text has changed
6148 ###############################################################################
6149 # Check if any new information was found
6150 if not self.config['overwrite']:
6151 for key in available_metadata.keys():
6152 if available_metadata[key] != meta_dict[key]:
6153 if available_metadata[key] == u'' and meta_dict[key] == None:
6154 continue
6155 if available_metadata[key] == u'' and meta_dict[key] == u'Unknown':
6156 continue
6157 try:
6158 self._displayMessage(
6159 u"1-At least (%s)'s value(%s) has changed new(%s)(%s) old(%s)(%s)\n" % (cfile['filename'], key, available_metadata[key], type(available_metadata[key]), meta_dict[key], type(meta_dict[key])))
6160 except:
6161 self._displayMessage(
6162 u"2-At least (%s)'s value(%s) has changed new(%s) old(%s)\n" % (cfile['filename'], key, type(available_metadata[key]), type(meta_dict[key])))
6163 break
6164 else:
6165 self._displayMessage(
6166 u"Nothing to update for video file(%s)\n" % cfile['filename']
6167 )
6168 continue
6169
6170 if self.config['simulation']:
6171 sys.stdout.write(
6172 u"Simulation MythTV DB update for video file(%s)\n" % cfile['filename']
6173 )
6174 for key in available_metadata.keys():
6175 print key," ", available_metadata[key]
6176 for key in genres_cast.keys():
6177 sys.stdout.write(u"Key(%s):(%s)\n" % (key, genres_cast[key]))
6178 else:
6179 sys.stdout.write('\n')
6180 else:
6181 # Clean up a few fields before updating Mythdb
6182 if available_metadata['showlevel'] == 0: # Allows mythvideo to display this video
6183 available_metadata['showlevel'] = 1
6184 Video(intid, db=mythvideo).update(available_metadata)
6185 num_mythdb_updates+=1
6186 videos_updated_metadata.append(cfile['filename'])
6187 for key in ['genres', 'cast', 'countries']:
6188 if key == 'genres' and len(cfile['categories']):
6189 genres_cast[key]+=cfile['categories']
6190 if genres_cast.has_key(key):
6191 self._addCastGenreCountry( genres_cast[key], Video(intid, db=mythvideo), key)
6192 self._displayMessage(
6193 u"Updated Mythdb for video file(%s)\n" % cfile['filename']
6194 )
6195 ###############################################################################
6196 # END of metadata updating the MythVideo record when graphics or text has changed
6197 ###############################################################################
6198
6199 sys.stdout.write(u"\nMythtv video database maintenance ends at : %s\n" % (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M"))
6200
6201 # Print statistics
6202 sys.stdout.write(u'\n------------------Statistics---------------\nNumber of video files processed .....(% 5d)\nNumber of Fanart graphics downloaded (% 5d)\nNumber of Poster graphics downloaded (% 5d)\nNumber of Banner graphics downloaded (% 5d)\nNumber of 2nd source graphics downld (% 5d)\nNumber of metadata downloads.........(% 5d)\nNumber of 2nd source metadata found .(% 5d)\nNumber of symbolic links created.....(% 5d)\nNumber of Myth database updates......(% 5d)\nNumber of undersized posters ........(% 5d)\nNumber of Movies using IMDB numbers .(% 5d)\n' % (num_processed, num_fanart_downloads, num_posters_downloads, num_banners_downloads, self.num_secondary_source_graphics_downloaded, num_episode_metadata_downloads, self.num_secondary_source_metadata_downloaded, num_symlinks_created, num_mythdb_updates, num_posters_below_min_size, num_movies_using_imdb_numbers))
6203
6204 if len(videos_updated_metadata):
6205 sys.stdout.write(u'\n--------------Updated Video Files----------\n' )
6206 for videofile in videos_updated_metadata:
6207 sys.stdout.write(u'%s\n' % videofile)
6208 if len(missing_inetref):
6209 sys.stdout.write(u'\n----------------No Inetref Found-----------\n' )
6210 for videofile in missing_inetref:
6211 sys.stdout.write(u'%s\n' % videofile)
6212 if len(videos_with_small_posters):
6213 sys.stdout.write(u'\n---------------Under sized Poster----------\n' )
6214 for videofile in videos_with_small_posters:
6215 sys.stdout.write(u'%s\n' % videofile)
6216 if len(videos_using_imdb_numbers):
6217 sys.stdout.write(u'\n---------------Movies with IMDB#s----------\n' )
6218 for videofile in videos_using_imdb_numbers:
6219 sys.stdout.write(u'%s\n' % videofile)
6220 return None
6221 # end processMythTvMetaData
6222
6223 def __repr__(self): # Just a place holder
6224 return self.config
6225 # end __repr__
6226
6227 # end MythTvMetaData
6228
6229 def simple_example():
6230 """Simple example of using jamu
6231 Displays the poster graphics URL(s) and episode meta data for the TV series Sanctuary, season 1
6232 episode 3
6233 returns None if there was no data found for the request TV series
6234 returns False if there is no TV series as specified
6235 returns a string with poster URLs and episode meta data
6236 """
6237 # Get an instance of the variable configuration information set to default values
6238 configuration = Configuration(interactive = True, debug = False)
6239
6240 # Set the type of data to be returned
6241 configuration.changeVariable('get_poster', True)
6242 configuration.changeVariable('get_ep_meta', True)
6243
6244 # Validate specific variables and set the TV series information
6245 configuration.validate_setVariables(['Sanctuary', '1', '3'])
6246
6247 # Get an instance of the tvdb process function and fetch the data
6248 process = Tvdatabase(configuration.config)
6249 results = process.processTVdatabaseRequests()
6250
6251 if results != None and results != False: # Print the returned data string to the stdout
6252 print process.processTVdatabaseRequests().encode('utf8')
6253 # end simple_example
6254
6255
6256 def main():
6257 """Support jamu from the command line
6258 returns True
6259 """
6260 parser = OptionParser(usage=u"%prog usage: jamu -hbueviflstdnmoCRFUDSGN [parameters]\n <series name/SID or 'series/SID and season number' or 'series/SID and season number and episode number' or 'series/SID and episode name' or video file/directory paired with destination directory'>")
6261
6262 parser.add_option( "-b", "--debug", action="store_true", default=False, dest="debug",
6263 help=u"Show debugging info")
6264 parser.add_option( "-u", "--usage", action="store_true", default=False, dest="usage",
6265 help=u"Display the six main uses for this jamu")
6266 parser.add_option( "-e", "--examples", action="store_true", default=False, dest="examples",
6267 help=u"Display examples for executing the jamu script")
6268 parser.add_option( "-v", "--version", action="store_true", default=False, dest="version",
6269 help=u"Display version and author information")
6270 parser.add_option( "-i", "--interactive", action="store_true", default=False, dest="interactive",
6271 help=u"Interactive mode allows selection of a specific Series from a series list")
6272 parser.add_option( "-f", "--flags_options", action="store_true", default=False,dest="flags_options",
6273 help=u"Display all variables and settings then exit")
6274 parser.add_option( "-l", "--language", metavar="LANGUAGE", default=u'en', dest="language",
6275 help=u"Select data that matches the specified language fall back to english if nothing found (e.g. 'es' Español, 'de' Deutsch ... etc)")
6276 parser.add_option( "-s", "--simulation", action="store_true", default=False, dest="simulation",
6277 help=u"Simulation (dry run), no downloads are performed or data bases altered")
6278 parser.add_option( "-t", "--toprated", action="store_true", default=False, dest="toprated",
6279 help=u"Only display/download the top rated TV Series graphics")
6280 parser.add_option( "-d", "--download", action="store_true", default=False, dest="download",
6281 help=u"Download and save the graphics and/or meta data")
6282 parser.add_option( "-n", "--nokeys", action="store_true", default=False, dest="nokeys",
6283 help=u"Do not add data type keys to data values when displaying data")
6284 parser.add_option( "-m", "--maximum", metavar="MAX", default=None, dest="maximum",
6285 help=u"Limit the number of graphics per type downloaded. e.g. --maximum=6")
6286 parser.add_option( "-o", "--overwrite", action="store_true", default=False, dest="overwrite",
6287 help=u"Overwrite any matching files already downloaded")
6288 parser.add_option( "-C", "--user_config", metavar="FILE", default="", dest="user_config",
6289 help=u"User specified configuration variables. e.g --user_config='~/.jamu/jamu.conf'")
6290 parser.add_option( "-F", "--filename", action="store_true", default=False, dest="ret_filename",
6291 help=u"Display a formated filename for an episode")
6292 parser.add_option( "-U", "--update", action="store_true", default=False, dest="update",
6293 help=u"Update a meta data file if local episode meta data is older than what is available on thetvdb.com")
6294 parser.add_option( "-D", "--mythtvdir", action="store_true", default=False, dest="mythtvdir",
6295 help=u"Store graphic files into the MythTV DB specified dirs")
6296 parser.add_option( "-M", "--mythtvmeta", action="store_true", default=False, dest="mythtvmeta",
6297 help=u"Add/update TV series episode or movie meta data in MythTV DB")
6298 parser.add_option( "-V", "--mythtv_verbose", action="store_true", default=False, dest="mythtv_verbose",
6299 help=u"Display verbose messages when performing MythTV metadata maintenance")
6300 parser.add_option( "-J", "--mythtvjanitor", action="store_true", default=False, dest="mythtvjanitor",
6301 help=u"Remove unused graphics (poster, fanart, banners) with the graphics janitor. Any graphics not associated with atleast one MythTV video file record is delected.")
6302 parser.add_option( "-N", "--mythtvNFS", action="store_true", default=False, dest="mythtvNFS",
6303 help=u"This option overrides Jamu's restrictions on processing NFS mounted Video and/or graphic files.")
6304 parser.add_option( "-I", "--mythtv_inetref", action="store_true", default=False, dest="mythtv_inetref",
6305 help=u"Find and interactively update any missing Interent reference numbers e.g. IMDB. This option is ONLY active if the -M option is also selected.")
6306 parser.add_option( "-W", "--mythtv_watched", action="store_true", default=False, dest="mythtv_watched",
6307 help=u"Download graphics for Scheduled and Recorded videos. This option is ONLY active if the -M option is also selected.")
6308 parser.add_option( "-G", "--mythtv_guess", action="store_true", default=False, dest="mythtv_guess",
6309 help=u"Guess at the inetref for a video. This option is ONLY active if the -M option is also selected.")
6310 parser.add_option( "-S", "--selected_data", metavar="TYPES", default=None, dest="selected_data",
6311 help=u"Select one of more data types to display or download, P-poster, B-Banner, F-Fanart, E-Episode data, I-Episode Image. e.g. --selected_data=PBFEI gets all types of data")
6312 parser.add_option( "-R", "--mythtv_ref_num", action="store_true", default=False, dest="mythtv_ref_num",
6313 help=u"Start an interactive session that ONLY adds the TVDB/TMDB reference numbers to when missing. No meta data or images will be concurrently downloaded.")
6314
6315 opts, series_season_ep = parser.parse_args()
6316
6317 if opts.debug:
6318 print "opts", opts
6319 print "\nargs", series_season_ep
6320
6321 # Set the default configuration values
6322 if opts.mythtv_inetref or opts.mythtv_ref_num:
6323 opts.interactive = True
6324 configuration = Configuration(interactive = opts.interactive, debug = opts.debug)
6325
6326 if opts.usage: # Display usage information
6327 sys.stdout.write(usage_txt+'\n')
6328 sys.exit(0)
6329
6330 if opts.examples: # Display example information
6331 sys.stdout.write(examples_txt+'\n')
6332 sys.exit(0)
6333
6334 if opts.version == True: # Display program information
6335 sys.stdout.write(u"\nTitle: (%s); Version: (%s); Author: (%s)\n%s\n" % (
6336 __title__, __version__, __author__, __purpose__ ))
6337 sys.exit(0)
6338
6339 # Verify that only one instance of the following options is running at any one time
6340 # Options (-M, -MW and -MG)
6341 options = u''
6342 if opts.mythtvmeta:
6343 options+=u'M'
6344 else:
6345 MythLog._setlevel('none') # There cannot be any logging messages with non -M options
6346 if opts.mythtvmeta and opts.mythtv_watched:
6347 options+=u'W'
6348 if opts.mythtvmeta and opts.mythtv_guess:
6349 options+=u'G'
6350 if opts.mythtvmeta and opts.mythtvjanitor: # No instance check with the janitor option
6351 options+=u'J'
6352 if opts.mythtvmeta and opts.mythtv_inetref: # No instance check with the interactive mode option
6353 options+=u'I'
6354 if options in [u'M', u'MW', u'MG']:
6355 jamu_instance = singleinstance(u'/tmp/Jamu_%s_instance.pid' % options)
6356 #
6357 # check is another instance of Jamu is running
6358 #
6359 if jamu_instance.alreadyrunning():
6360 print u'\n! Error: An instance of Jamu (-%s) is already running only one instance can run at a time.\nOne of the meta data sources may be off-line or very slow.\n' % options
6361 sys.exit(0)
6362
6363 # Message the user that they are using incompatible options with the -MW option
6364 if opts.mythtvmeta and opts.mythtv_watched and (opts.mythtv_inetref or opts.interactive):
6365 print u'\n! Error: There us no Interactive mode (-I or -i) for the Jamu (-MW) option.\nPlease change your options and try again.\n'
6366 sys.exit(1)
6367
6368 # Message the user that they are using incompatible options -R and -I or -i
6369 if opts.mythtvmeta and opts.mythtv_ref_num and opts.mythtv_inetref:
6370 print u'\n! Error: The (-R) and (-I) options are mutually exclusive.\nPlease change your options and try again.\n'
6371 sys.exit(1)
6372
6373 # Apply any command line switches
6374 configuration.changeVariable('local_language', opts.language)
6375 configuration.changeVariable('simulation', opts.simulation)
6376 configuration.changeVariable('toprated', opts.toprated)
6377 configuration.changeVariable('download', opts.download)
6378 configuration.changeVariable('nokeys', opts.nokeys)
6379 configuration.changeVariable('maximum', opts.maximum)
6380 configuration.changeVariable('overwrite', opts.overwrite)
6381 configuration.changeVariable('ret_filename', opts.ret_filename)
6382 configuration.changeVariable('update', opts.update)
6383 configuration.changeVariable('mythtvdir', opts.mythtvdir)
6384 configuration.changeVariable('mythtvmeta', opts.mythtvmeta)
6385 configuration.changeVariable('mythtv_inetref', opts.mythtv_inetref)
6386 configuration.changeVariable('mythtv_ref_num', opts.mythtv_ref_num)
6387 configuration.changeVariable('mythtv_watched', opts.mythtv_watched)
6388 configuration.changeVariable('mythtv_guess', opts.mythtv_guess)
6389 configuration.changeVariable('mythtv_verbose', opts.mythtv_verbose)
6390 configuration.changeVariable('mythtvjanitor', opts.mythtvjanitor)
6391 configuration.changeVariable('mythtvNFS', opts.mythtvNFS)
6392 configuration.changeVariable('data_flags', opts.selected_data)
6393
6394 # Check if the user wants to change options via a configuration file
6395 if opts.user_config != '': # Did the user want to override the default config file name/location
6396 configuration.setUseroptions(opts.user_config)
6397 else:
6398 default_config = u"%s/%s" % (os.path.expanduser(u"~"), u".mythtv/jamu.conf")
6399 if os.path.isfile(default_config):
6400 configuration.setUseroptions(default_config)
6401 else:
6402 print u"\nThere was no default Jamu configuration file found (%s)\n" % default_config
6403
6404 if opts.flags_options: # Display option variables
6405 if len(series_season_ep):
6406 configuration.validate_setVariables(series_season_ep)
6407 else:
6408 configuration.validate_setVariables(['FAKE SERIES NAME','FAKE EPISODE NAME'])
6409 configuration.displayOptions()
6410 sys.exit(0)
6411
6412 # Validate specific variables
6413 configuration.validate_setVariables(series_season_ep)
6414
6415 if configuration.config['mythtvmeta']:
6416 process = MythTvMetaData(configuration.config)
6417 process.processMythTvMetaData()
6418 elif configuration.config['video_dir']:
6419 process = VideoFiles(configuration.config)
6420 results = process.processFileOrDirectory()
6421 if results != None and results != False:
6422 print process.processFileOrDirectory().encode('utf8')
6423 else:
6424 process = Tvdatabase(configuration.config)
6425 results = process.processTVdatabaseRequests()
6426 if results != None and results != False:
6427 print process.processTVdatabaseRequests().encode('utf8')
6428 return True
6429 # end main
6430
6431 if __name__ == "__main__":
6432 main()