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