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