|
MythTV
0.26-pre
|
00001 #!/usr/bin/env python 00002 # -*- coding: UTF-8 -*- 00003 # ---------------------- 00004 # Name: ttvdb.py 00005 # Python Script 00006 # Author: R.D. Vaughan 00007 # Purpose: 00008 # This python script is intended to perform TV series data lookups 00009 # based on information found on the http://thetvdb.com/ website. It 00010 # follows the MythTV standards set for the movie data 00011 # lookups. e.g. the perl script "tmdb.pl" used to access themoviedb.com 00012 # This script uses the python module tvdb_api.py (v1.0 or higher) found at 00013 # http://pypi.python.org/pypi?%3Aaction=search&term=tvnamer&submit=search 00014 # thanks to the authors of this excellant module. 00015 # The tvdb_api.py module uses the full access XML api published by 00016 # thetvdb.com see: 00017 # http://thetvdb.com/wiki/index.php?title=Programmers_API 00018 # Users of this script are encouraged to populate thetvdb.com with TV show 00019 # information, posters, fan art and banners. The richer the source the more 00020 # valuable the script. 00021 # This python script was modified based on the "tvnamer.py" created by 00022 # "dbr/Ben" who is also 00023 # the author of the "tvdb_api.py" module. "tvnamer.py" is used to rename avi 00024 # files with series/episode information found at thetvdb.com 00025 # Command example: 00026 # See help (-u and -h) options 00027 # 00028 # Design: 00029 # 1) Verify the command line options (display help or version and exit) 00030 # 2) Verify that thetvdb.com has the series or series_season_ep being 00031 # requested exit if does not exit 00032 # 3) Find the requested information and send to stdout if any found 00033 # 00034 # 00035 # License:Creative Commons GNU GPL v2 00036 # (http://creativecommons.org/licenses/GPL/2.0/) 00037 #------------------------------------- 00038 __title__ ="TheTVDB.com"; 00039 __author__="R.D.Vaughan" 00040 __version__="1.1.5" 00041 # Version .1 Initial development 00042 # Version .2 Add an option to get season and episode numbers from ep name 00043 # Version .3 Cleaned up the documentation and added a usage display option 00044 # Version .4 Added override formating of the number option (-N) 00045 # Version .5 Added a check that tvdb_api.py dependancies are installed 00046 # Version .6 Added -M Series List functionality 00047 # Version .7 Added -o Series name override file functionality 00048 # Version .8 Removed a dependancy, fixed caching for multiple users and 00049 # used better method of supporting the -M option with tvdb_api 00050 # Version .8.1 Cleaned up documentation errors. 00051 # Version .8.2 Added the series name to output of meta data -D 00052 # Version .8.3 Patched tv_api so even episode image is fully qualified URL 00053 # Version .8.4 Fixed bug in -N when multiple episodes are returned from 00054 # a search on episode name. 00055 # Version .8.5 Made option -N more flexible in finding a matching episode 00056 # Version .8.6 Add option -t for top rated graphics (-P,B,F) for a series 00057 # Add option -l to filter graphics on the local language 00058 # Add season level granularity to Poster and Banner URLs 00059 # Changed the override file location to be specified on the 00060 # command line along with the option -o. 00061 # Increased the amount of massaging of episode data to improve 00062 # compatiblilty with data bases. 00063 # Changed the default season episode number format to SxxExx 00064 # Add an option (-n) to return the season numbers for a series 00065 # Added passing either a thetvdb.com SID (series identifcation 00066 # number) or series name for all functions except -M list. 00067 # Now ALL available episode meta data is returned. This 00068 # includes meta data that MythTv DB does not currently store. 00069 # Meta data 'Year' now derived from year in release date. 00070 # Version .8.7 Fixed a bug with unicode meta data handling 00071 # Version .8.8 Replaced the old configuration file with a true conf file 00072 # Version .8.9 Add option -m to better conform to mythvideo standards 00073 # Version .9.0 Now when a season level Banner is not found then the 00074 # top rated series level graphics is returned instead. This 00075 # feature had previously been available for posters only. 00076 # Add runtime to episode meta data (-D). It is always the 00077 # same for each episode as the information is only available 00078 # from the series itself. 00079 # Added the TV Series cast members as part of episode 00080 # meta data. Option (-D). 00081 # Added TV Series series genres as part of episode 00082 # meta data. Option (-D). 00083 # Resync with tvdb_api getting bug fixes and new features. 00084 # Add episode data download to a specific language see 00085 # -l 'es' option. If there is no data for a languages episode 00086 # then any engish episode data is returned. English is default. 00087 # The -M is still only in English. Waiting on tvdb_api fix. 00088 # Version .9.1 Bug fix stdio abort when no genre exists for TV series 00089 # Version .9.2 Bug fix stdio abort when no cast exists for TV series 00090 # Version .9.3 Changed option -N when episodes partially match each 00091 # combination of season/episode numbers are returned. This was 00092 # added to deal with episodes which have a "(1)" trailing 00093 # the episode name. An episode in more than one part. 00094 # Version .9.4 Option -M can now list multi-language TV Series 00095 # Version .9.5 "Director" metadata field returned as "None" is changed to 00096 # "Unknown". 00097 # File name parsing was changed to use multi-language capable 00098 # regex patterns 00099 # Version .9.6 Synced up to the 1.0 release of tvdb_api 00100 # Added a tvdb_api version check and abort if not at least v1.0 00101 # Changed to new tvdb_api's method of assigning the tvdb api key 00102 # Version .9.7 Account for TVDB increasing the number of digits in their 00103 # SID number (now greater then 5 00104 # e,g, "Defying Gravity" is SID 104581) 00105 # Version .9.8 Added a (-S) option for requesting a thetvdb 00106 # episode screen shot 00107 # Version .9.9 Fixed the -S option when NO episode image exists 00108 # Version 1.0. Removed LF and replace with a space for all TVDB metatdata 00109 # fields 00110 # Version 1.0.1 Return all graphics (series and season) in the order 00111 # highest to lowest as rated by users 00112 # Version 1.0.2 Added better error messages to config file checking. Updated to 00113 # v1.0.2 tvdb_api which contained fixes for concurrent instances 00114 # of ttvdb.py generated by MythVideo. 00115 # Version 1.0.3 Conform to new -D standards which return all graphics URLs along 00116 # with text meta data. Also Posters, Banners and Fan art have one or 00117 # comma separated URLs as one continuous string. 00118 # Version 1.0.4 Poster Should be Coverart instead. 00119 # Version 1.0.5 Added the TVDB URL to the episode metadata 00120 # Version 1.0.6 When the language is invalid ignore language and continue processing 00121 # Changed all exit codes to be 0 for passed and 1 for failed 00122 # Removed duplicates when using the -M option and the -l option 00123 # Version 1.0.7 Change over to the installed TTVDB api library 00124 # Fixed cast name bad data abort reported in ticket #7957 00125 # Fixed ticket #7900 - There is now fall back graphics when a 00126 # language is specified and there is no images for that language. 00127 # New default location and name of the ttvdb.conf file is '~/.mythtv/ttvdb.conf'. The command 00128 # line option "-c" can still override the default location and name. 00129 # Version 1.0.8 Removed any stderr messages for non-critical events. They cause dual pop-ups in MythVideo. 00130 # Version 1.0.9 Removed another stderr messages when a non-supported language code is passed. 00131 # Version 1.1.0 Added support for XML output. See: 00132 # http://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format 00133 # Version 1.1.1 Make XML output the default. 00134 # Version 1.1.2 Convert version information to XML 00135 # Version 1.1.3 Implement fuzzy matching for episode name lookup 00136 # Version 1.1.4 Add test mode (replaces --toprated) 00137 # Version 1.1.5 Add the -C (collection option) with corresponding XML output 00138 # and add a <collectionref> XML tag to Search and Query XML output 00139 00140 usage_txt=''' 00141 Usage: ttvdb.py usage: ttvdb -hdruviomMPFBDSC [parameters] 00142 <series name or 'series and season number' or 'series and season number and episode number'> 00143 00144 For details on using ttvdb with Mythvideo see the ttvdb wiki page at: 00145 http://www.mythtv.org/wiki/Ttvdb.py 00146 00147 Options: 00148 -h, --help show this help message and exit 00149 -d, --debug Show debugging info 00150 -r, --raw Dump raw data only 00151 -u, --usage Display examples for executing the ttvdb script 00152 -v, --version Display version and author 00153 -t Test mode, to check for installed dependencies 00154 -i, --interactive Interaction mode (allows selection of a specific 00155 Series) 00156 -c FILE, --configure=FILE 00157 Use configuration settings 00158 -l LANGUAGE, --language=LANGUAGE 00159 Select data that matches the specified language fall 00160 back to english if nothing found (e.g. 'es' Español, 00161 'de' Deutsch ... etc) 00162 -n, --num_seasons Return the season numbers for a series 00163 -m, --mythvideo Conform to mythvideo standards when processing -M, -P, 00164 -F and -D 00165 -M, --list Get matching TV Series list 00166 -P, --poster Get Series Poster URL(s) 00167 -F, --fanart Get Series fan art URL(s) 00168 -B, --backdrop Get Series banner/backdrop URL(s) 00169 -S, --screenshot Get Series episode screenshot URL 00170 -D, --data Get Series episode data 00171 -N, --numbers Get Season and Episode numbers 00172 -C, --collection Get A TV Series (collection) series specific information 00173 00174 Command examples: 00175 (Return the banner graphics for a series) 00176 > ttvdb -B "Sanctuary" 00177 Banner:http://www.thetvdb.com/banners/graphical/80159-g2.jpg,http://www.thetvdb.com/banners/graphical/80159-g3.jpg,http://www.thetvdb.com/banners/graphical/80159-g.jpg 00178 00179 (Return the banner graphics for a Series specific to a season) 00180 > ttvdb -B "SG-1" 1 00181 Banner:http://www.thetvdb.com/banners/graphical/72449-g2.jpg,http://www.thetvdb.com/banners/graphical/185-g3.jpg,http://www.thetvdb.com/banners/graphical/185-g2.jpg,http://www.thetvdb.com/banners/graphical/185-g.jpg,http://www.thetvdb.com/banners/graphical/72449-g.jpg,http://www.thetvdb.com/banners/text/185.jpg 00182 00183 (Return the screen shot graphic for a Series Episode) 00184 > ttvdb -S "SG-1" 1 10 00185 http://www.thetvdb.com/banners/episodes/72449/85759.jpg 00186 00187 (Return the banner graphics for a SID (series ID) specific to a season) 00188 (SID "72449" is specific for the series "SG-1") 00189 > ttvdb -B 72449 1 00190 Banner:http://www.thetvdb.com/banners/graphical/72449-g2.jpg,http://www.thetvdb.com/banners/graphical/185-g3.jpg,http://www.thetvdb.com/banners/graphical/185-g2.jpg,http://www.thetvdb.com/banners/graphical/185-g.jpg,http://www.thetvdb.com/banners/graphical/72449-g.jpg,http://www.thetvdb.com/banners/text/185.jpg 00191 00192 (Return the banner graphics for a file name) 00193 > ttvdb -B "Stargate SG-1 - S08E03 - Lockdown" 00194 Banner:http://www.thetvdb.com/banners/graphical/72449-g2.jpg,http://www.thetvdb.com/banners/graphical/185-g3.jpg,http://www.thetvdb.com/banners/graphical/185-g2.jpg,http://www.thetvdb.com/banners/graphical/185-g.jpg,http://www.thetvdb.com/banners/graphical/72449-g.jpg,http://www.thetvdb.com/banners/text/185.jpg 00195 00196 (Return the posters, banners and fan art for a series) 00197 > ttvdb -PFB "Sanctuary" 00198 Coverart:http://www.thetvdb.com/banners/posters/80159-2.jpg,http://www.thetvdb.com/banners/posters/80159-1.jpg 00199 Fanart:http://www.thetvdb.com/banners/fanart/original/80159-2.jpg,http://www.thetvdb.com/banners/fanart/original/80159-1.jpg,http://www.thetvdb.com/banners/fanart/original/80159-8.jpg,http://www.thetvdb.com/banners/fanart/original/80159-6.jpg,http://www.thetvdb.com/banners/fanart/original/80159-5.jpg,http://www.thetvdb.com/banners/fanart/original/80159-9.jpg,http://www.thetvdb.com/banners/fanart/original/80159-3.jpg,http://www.thetvdb.com/banners/fanart/original/80159-7.jpg,http://www.thetvdb.com/banners/fanart/original/80159-4.jpg 00200 Banner:http://www.thetvdb.com/banners/graphical/80159-g2.jpg,http://www.thetvdb.com/banners/graphical/80159-g3.jpg,http://www.thetvdb.com/banners/graphical/80159-g.jpg 00201 00202 (Return thetvdb.com's top rated poster, banner and fan art for a TV Series) 00203 (NOTE: If there is no graphic for a type or any graphics at all then those types are not returned) 00204 > ttvdb -tPFB "Stargate SG-1" 00205 Coverart:http://www.thetvdb.com/banners/posters/72449-1.jpg 00206 Fanart:http://www.thetvdb.com/banners/fanart/original/72449-1.jpg 00207 Banner:http://www.thetvdb.com/banners/graphical/185-g3.jpg 00208 > ttvdb -tB "Night Gallery" 00209 http://www.thetvdb.com/banners/blank/70382.jpg 00210 00211 (Return graphics only matching the local language for a TV series) 00212 (In this case banner 73739-g9.jpg is not included because it does not match the language 'en') 00213 > ttvdb -Bl en "Lost" 00214 Banner:http://www.thetvdb.com/banners/graphical/73739-g4.jpg,http://www.thetvdb.com/banners/graphical/73739-g.jpg,http://www.thetvdb.com/banners/graphical/73739-g6.jpg,http://www.thetvdb.com/banners/graphical/73739-g8.jpg,http://www.thetvdb.com/banners/graphical/73739-g3.jpg,http://www.thetvdb.com/banners/graphical/73739-g7.jpg,http://www.thetvdb.com/banners/graphical/73739-g5.jpg,http://www.thetvdb.com/banners/graphical/24313-g2.jpg,http://www.thetvdb.com/banners/graphical/24313-g.jpg,http://www.thetvdb.com/banners/graphical/73739-g10.jpg,http://www.thetvdb.com/banners/graphical/73739-g2.jpg 00215 00216 (Return a season and episode numbers using the override file to identify the series as the US version) 00217 > ttvdb -N --configure="/home/user/.tvdb/tvdb.conf" "Eleventh Hour" "H2O" 00218 <?xml version='1.0' encoding='UTF-8'?> 00219 <metadata> 00220 <item> 00221 <title>Eleventh Hour (US)</title> 00222 <subtitle>H2O</subtitle> 00223 <language>en</language> 00224 <description>An epidemic of sudden, violent outbursts by law-abiding citizens draws Dr. Jacob Hood to a quiet Texas community to investigate - but he soon succumbs to the same erratic behavior.</description> 00225 <season>1</season> 00226 <episode>10</episode> 00227 ... 00228 <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/83066-4.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/83066-4.jpg" width="1280" height="720"/> 00229 <image type="banner" url="http://www.thetvdb.com/banners/graphical/83066-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/83066-g.jpg"/> 00230 </images> 00231 </item> 00232 </metadata> 00233 00234 (Return the season numbers for a series) 00235 > ttvdb --configure="/home/user/.tvdb/tvdb.conf" -n "SG-1" 00236 0,1,2,3,4,5,6,7,8,9,10 00237 00238 (Return the meta data for a specific series/season/episode) 00239 > ttvdb.py -D 80159 2 2 00240 <?xml version='1.0' encoding='UTF-8'?> 00241 <metadata> 00242 <item> 00243 <title>Sanctuary</title> 00244 <subtitle>End of Nights (2)</subtitle> 00245 <language>en</language> 00246 <description>Furious at being duped into a trap, Magnus (AMANDA TAPPING) takes on Kate (AGAM DARSHI), demanding information and complete access to her Cabal contacts. The Cabal’s true agenda is revealed and Magnus realizes that they are not only holding Ashley (EMILIE ULLERUP) as ransom to obtain complete control of the Sanctuary Network, but turning her into the ultimate weapon. Now transformed into a Super Abnormal with devastating powers, Ashley and her newly cloned fighters begin their onslaught, destroying Sanctuaries in cities around the world. Tesla (JONATHON YOUNG) and Henry (RYAN ROBBINS) attempt to create a weapon that can stop the attacks…without killing Ashley. As the team prepares to defend the Sanctuary with Tesla’s new weapon, Magnus must come to the realization that they may not be able to stop the Cabal’s attacks without harming Ashley. She realizes she might have to choose between saving her only daughter, or losing the Sanctuary and all the lives and secrets within it.</description> 00247 <season>2</season> 00248 <episode>2</episode> 00249 <certifications> 00250 <certification locale="us" name="TV-PG"/> 00251 </certifications> 00252 <categories> 00253 <category type="genre" name="Action and Adventure"/> 00254 <category type="genre" name="Science-Fiction"/> 00255 </categories> 00256 <studios> 00257 <studio name="SciFi"/> 00258 </studios> 00259 ... 00260 <image type="banner" url="http://www.thetvdb.com/banners/graphical/80159-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/80159-g.jpg"/> 00261 </images> 00262 </item> 00263 </metadata> 00264 00265 (Return a list of "thetv.com series id and series name" that contain specific search word(s) ) 00266 (!! Be careful with this option as poorly defined search words can result in large lists being returned !!) 00267 > ttvdb.py -M "night a" 00268 <?xml version='1.0' encoding='UTF-8'?> 00269 <metadata> 00270 <item> 00271 <language>en</language> 00272 <title>Love on a Saturday Night</title> 00273 <inetref>74382</inetref> 00274 <releasedate>2004-02-01</releasedate> 00275 </item> 00276 <item> 00277 <language>en</language> 00278 <title>A Night on Mount Edna</title> 00279 <inetref>108281</inetref> 00280 </item> 00281 <item> 00282 <language>en</language> 00283 <title>A Night at the Office</title> 00284 <inetref>118511</inetref> 00285 <description>On August 11th 2009, it was announced that the cast of The Office would be reuniting for a special, called "A Night at The Office", available at BBC2 and online.</description> 00286 <releasedate>2009-01-01</releasedate> 00287 <images> 00288 <image type="banner" url="http://www.thetvdb.com/banners/graphical/118511-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/118511-g.jpg"/> 00289 </images> 00290 </item> 00291 <item> 00292 <language>en</language> 00293 <title>Star For A Night</title> 00294 <inetref>71476</inetref> 00295 <releasedate>1999-01-01</releasedate> 00296 </item> 00297 </metadata> 00298 00299 (Return TV series collection data of "thetv.com series id" for a specified language) 00300 > ttvdb.py -l de -C 80159 00301 <?xml version='1.0' encoding='UTF-8'?> 00302 <metadata> 00303 <item> 00304 <language>de</language> 00305 <title>Sanctuary</title> 00306 <network>Syfy</network> 00307 <airday>Friday</airday> 00308 <airtime>22:00</airtime> 00309 <description>Dr. Helen Magnus ist eine so brillante wie geheimnisvolle Wissenschaftlerin die sich mit den Kreaturen der Nacht beschäftigt. In ihrem Unterschlupf - genannt "Sanctuary" - hat sie ein Team versammelt, das seltsame und furchteinflößende Ungeheuer untersucht, die mit den Menschen auf der Erde leben. Konfrontiert mit ihren düstersten Ängsten und ihren schlimmsten Alpträumen versucht das Sanctuary-Team, die Welt vor den Monstern - und die Monster vor der Welt zu schützen.</description> 00310 <certifications> 00311 <certification locale="us" name="TV-PG"/> 00312 </certifications> 00313 <categories> 00314 <category type="genre" name="Action and Adventure"/> 00315 <category type="genre" name="Science-Fiction"/> 00316 </categories> 00317 <studios> 00318 <studio name="Syfy"/> 00319 </studios> 00320 <runtime>60</runtime> 00321 <inetref>80159</inetref> 00322 <imdb>0965394</imdb> 00323 <tmsref>EP01085421</tmsref> 00324 <userrating>8.0</userrating> 00325 <ratingcount>128</ratingcount> 00326 <year>2007</year> 00327 <releasedate>2007-05-14</releasedate> 00328 <lastupdated>Fri, 17 Feb 2012 16:57:02 GMT</lastupdated> 00329 <status>Continuing</status> 00330 <images> 00331 <image type="coverart" url="http://www.thetvdb.com/banners/posters/80159-4.jpg" thumb="http://www.thetvdb.com/banners/_cache/posters/80159-4.jpg"/> 00332 <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-8.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-8.jpg"/> 00333 <image type="banner" url="http://www.thetvdb.com/banners/graphical/80159-g6.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/80159-g6.jpg"/> 00334 </images> 00335 </item> 00336 </metadata> 00337 ''' 00338 # Episode keys that can be used in a episode data/information search. 00339 # All keys are currently being used. 00340 ''' 00341 'episodenumber' 00342 'rating' 00343 'overview' 00344 'dvd_episodenumber' 00345 'dvd_discid' 00346 'combined_episodenumber' 00347 'epimgflag' 00348 'id' 00349 'seasonid' 00350 'seasonnumber' 00351 'writer' 00352 'lastupdated' 00353 'filename' 00354 'absolute_number' 00355 'combined_season' 00356 'imdb_id' 00357 'director' 00358 'dvd_chapter' 00359 'dvd_season' 00360 'gueststars' 00361 'seriesid' 00362 'language' 00363 'productioncode' 00364 'firstaired' 00365 'episodename' 00366 ''' 00367 00368 00369 # System modules 00370 import sys, os, re, locale, ConfigParser 00371 from optparse import OptionParser 00372 from copy import deepcopy 00373 00374 # Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available 00375 try: 00376 # thetvdb.com specific modules 00377 import MythTV.ttvdb.tvdb_ui as tvdb_ui 00378 # from tvdb_api import Tvdb 00379 import MythTV.ttvdb.tvdb_api as tvdb_api 00380 from MythTV.ttvdb.tvdb_exceptions import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_episodenotfound, tvdb_attributenotfound, tvdb_userabort) 00381 00382 # verify version of tvdbapi to make sure it is at least 1.0 00383 if tvdb_api.__version__ < '1.0': 00384 print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api.__version__ 00385 raise 00386 except Exception, e: 00387 print ''' 00388 The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py. 00389 They should have been installed along with the MythTV python bindings. 00390 Error:(%s) 00391 ''' % e 00392 sys.exit(1) 00393 00394 try: 00395 from MythTV.utility import levenshtein 00396 except Exception, e: 00397 print """Could not import levenshtein string distance method from MythTV Python Bindings 00398 Error:(%s) 00399 """ % e 00400 sys.exit(1) 00401 00402 try: 00403 from StringIO import StringIO 00404 from lxml import etree as etree 00405 except Exception, e: 00406 sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e) 00407 sys.exit(1) 00408 00409 # Check that the lxml library is current enough 00410 # From the lxml documents it states: (http://codespeak.net/lxml/installation.html) 00411 # "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later" 00412 # Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package 00413 version = '' 00414 for digit in etree.LIBXML_VERSION: 00415 version+=str(digit)+'.' 00416 version = version[:-1] 00417 if version < '2.7.2': 00418 sys.stderr.write(u''' 00419 ! Error - The installed version of the "lxml" python library "libxml" version is too old. 00420 At least "libxml" version 2.7.2 must be installed. Your version is (%s). 00421 ''' % version) 00422 sys.exit(1) 00423 00424 00425 # Global variables 00426 http_find="http://www.thetvdb.com" 00427 http_replace="http://www.thetvdb.com" #Keep replace code "just in case" 00428 00429 logfile="/tmp/ttvdb.log" 00430 00431 name_parse=[ 00432 # foo_[s01]_[e01] 00433 re.compile('''^(.+?)[ \._\-]\[[Ss]([0-9]+?)\]_\[[Ee]([0-9]+?)\]?[^\\/]*$'''), 00434 # foo.1x09* 00435 re.compile('''^(.+?)[ \._\-]\[?([0-9]+)x([0-9]+)[^\\/]*$'''), 00436 # foo.s01.e01, foo.s01_e01 00437 re.compile('''^(.+?)[ \._\-][Ss]([0-9]+)[\.\- ]?[Ee]([0-9]+)[^\\/]*$'''), 00438 # foo.103* 00439 re.compile('''^(.+)[ \._\-]([0-9]{1})([0-9]{2})[\._ -][^\\/]*$'''), 00440 # foo.0103* 00441 re.compile('''^(.+)[ \._\-]([0-9]{2})([0-9]{2,3})[\._ -][^\\/]*$'''), 00442 ] # contains regex parsing filename parsing strings used to extract info from video filenames 00443 00444 # Episode meta data that is massaged 00445 massage={'writer':'|','director':'|', 'overview':'&', 'gueststars':'|' } 00446 # Keys and titles used for episode data (option '-D') 00447 data_keys =['seasonnumber','episodenumber','episodename','firstaired','director','overview','rating','writer','filename','language' ] 00448 data_titles=['Season:','Episode:','Subtitle:','ReleaseDate:','Director:','Plot:','UserRating:','Writers:','Screenshot:','Language:' ] 00449 # High level dictionay keys for select graphics URL(s) 00450 fanart_key='fanart' 00451 banner_key='series' 00452 poster_key='poster' 00453 season_key='season' 00454 # Lower level dictionay keys for select graphics URL(s) 00455 poster_series_key='680x1000' 00456 poster_season_key='season' 00457 fanart_hires_key='1920x1080' 00458 fanart_lowres_key='1280x720' 00459 banner_series_key='graphical' 00460 banner_season_key='seasonwide' 00461 # Type of graphics being requested 00462 poster_type='Poster' 00463 fanart_type='Fanart' 00464 banner_type='Banner' 00465 screenshot_request = False 00466 00467 # Cache directory name specific to the user. This avoids permission denied error with a common cache dirs 00468 cache_dir="/tmp/tvdb_api_%s/" % os.geteuid() 00469 00470 def _can_int(x): 00471 """Takes a string, checks if it is numeric. 00472 >>> _can_int("2") 00473 True 00474 >>> _can_int("A test") 00475 False 00476 """ 00477 try: 00478 int(x) 00479 except ValueError: 00480 return False 00481 else: 00482 return True 00483 # end _can_int 00484 00485 def debuglog(message): 00486 message+='\n' 00487 target_socket = open(logfile, "a") 00488 target_socket.write(message) 00489 target_socket.close() 00490 return 00491 # end debuglog 00492 00493 class OutStreamEncoder(object): 00494 """Wraps a stream with an encoder""" 00495 def __init__(self, outstream, encoding=None): 00496 self.out = outstream 00497 if not encoding: 00498 self.encoding = sys.getfilesystemencoding() 00499 else: 00500 self.encoding = encoding 00501 00502 def write(self, obj): 00503 """Wraps the output stream, encoding Unicode strings with the specified encoding""" 00504 if isinstance(obj, unicode): 00505 self.out.write(obj.encode(self.encoding)) 00506 else: 00507 self.out.write(obj) 00508 00509 def __getattr__(self, attr): 00510 """Delegate everything but write to the stream""" 00511 return getattr(self.out, attr) 00512 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8') 00513 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8') 00514 00515 # modified Show class implementing a fuzzy search 00516 class Show( tvdb_api.Show ): 00517 def fuzzysearch(self, term = None, key = None): 00518 results = [] 00519 for cur_season in self.values(): 00520 searchresult = cur_season.fuzzysearch(term = term, key = key) 00521 if len(searchresult) != 0: 00522 results.extend(searchresult) 00523 return results 00524 # end Show 00525 00526 # modified Season class implementing a fuzzy search 00527 class Season( tvdb_api.Season ): 00528 def fuzzysearch(self, term = None, key = None): 00529 results = [] 00530 for episode in self.values(): 00531 searchresult = episode.fuzzysearch(term = term, key = key) 00532 if searchresult is not None: 00533 results.append(searchresult) 00534 return results 00535 # end Season 00536 00537 # modified Episode class implementing a fuzzy search 00538 class Episode( tvdb_api.Episode ): 00539 _re_strippart = re.compile('(.*) \([0-9]+\)') 00540 def fuzzysearch(self, term = None, key = None): 00541 if term == None: 00542 raise TypeError("must supply string to search for (contents)") 00543 00544 term = unicode(term).lower() 00545 for cur_key, cur_value in self.items(): 00546 cur_key, cur_value = [unicode(a).lower() for a in [cur_key, cur_value]] 00547 if key is not None and cur_key != key: 00548 continue 00549 distance = levenshtein(cur_value, term) 00550 if distance <= 3: 00551 # handle most matches 00552 self.distance = distance 00553 return self 00554 if distance <= 5: 00555 # handle part numbers, 'subtitle (nn)' 00556 match = self._re_strippart.match(cur_value) 00557 if match: 00558 tmp = match.group(1) 00559 if levenshtein(tmp, term) <= 3: 00560 self.distance = distance 00561 return self 00562 return None 00563 #end Episode 00564 00565 # modified Tvdb API class using modified show classes 00566 class Tvdb( tvdb_api.Tvdb ): 00567 def series_by_sid(self, sid): 00568 "Lookup a series via it's sid" 00569 seriesid = 'sid:' + sid 00570 if not self.corrections.has_key(seriesid): 00571 self._getShowData(sid) 00572 self.corrections[seriesid] = sid 00573 return self.shows[sid] 00574 #end series_by_sid 00575 00576 # override the existing method, using modified show classes 00577 def _setItem(self, sid, seas, ep, attrib, value): 00578 if sid not in self.shows: 00579 self.shows[sid] = Show() 00580 if seas not in self.shows[sid]: 00581 self.shows[sid][seas] = Season() 00582 if ep not in self.shows[sid][seas]: 00583 self.shows[sid][seas][ep] = Episode() 00584 self.shows[sid][seas][ep][attrib] = value 00585 #end _setItem 00586 00587 # override the existing method, using modified show class 00588 def _setShowData(self, sid, key, value): 00589 if sid not in self.shows: 00590 self.shows[sid] = Show() 00591 self.shows[sid].data[key] = value 00592 #end _setShowData 00593 #end Tvdb 00594 00595 # Search for a series by SID or Series name 00596 def search_for_series(tvdb, sid_or_name): 00597 "Get series data by sid or series name of the Tv show" 00598 if SID == True: 00599 return tvdb.series_by_sid(sid_or_name) 00600 else: 00601 return tvdb[sid_or_name] 00602 # end search_for_series 00603 00604 # Verify that a Series or Series and Season exists on thetvdb.com 00605 def searchseries(t, opts, series_season_ep): 00606 global SID 00607 series_name='' 00608 if opts.configure != "" and override.has_key(series_season_ep[0].lower()): 00609 series_name=override[series_season_ep[0].lower()][0] # Override series name 00610 else: 00611 series_name=series_season_ep[0] # Leave the series name alone 00612 try: 00613 # Search for the series or series & season or series & season & episode 00614 if len(series_season_ep)>1: 00615 if len(series_season_ep)>2: # series & season & episode 00616 seriesfound=search_for_series(t, series_name)[ int(series_season_ep[1]) ][ int(series_season_ep[2]) ] 00617 else: 00618 seriesfound=search_for_series(t, series_name)[ int(series_season_ep[1]) ] # series & season 00619 else: 00620 seriesfound=search_for_series(t, series_name) # Series only 00621 except tvdb_shownotfound: 00622 # No such show found. 00623 # Use the show-name from the files name, and None as the ep name 00624 sys.exit(0) 00625 except (tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound): 00626 # The season, episode or name wasn't found, but the show was. 00627 # Use the corrected show-name, but no episode name. 00628 sys.exit(0) 00629 except tvdb_error, errormsg: 00630 # Error communicating with thetvdb.com 00631 if SID == True: # Maybe the digits were a series name (e.g. 90210) 00632 SID = False 00633 return searchseries(t, opts, series_season_ep) 00634 sys.exit(0) 00635 except tvdb_userabort, errormsg: 00636 # User aborted selection (q or ^c) 00637 print "\n", errormsg 00638 sys.exit(0) 00639 else: 00640 if opts.raw==True: 00641 print "="*20 00642 print "Raw Series Data:\n" 00643 if len(series_season_ep)>1: 00644 print t[ series_name ][ int(series_season_ep[1]) ] 00645 else: 00646 print t[ series_name ] 00647 print "="*20 00648 return(seriesfound) 00649 # end searchseries 00650 00651 # Retrieve Poster or Fan Art or Banner graphics URL(s) 00652 def get_graphics(t, opts, series_season_ep, graphics_type, single_option, language=False): 00653 banners='_banners' 00654 series_name='' 00655 graphics=[] 00656 if opts.configure != "" and override.has_key(series_season_ep[0].lower()): 00657 series_name=override[series_season_ep[0].lower()][0] # Override series name 00658 else: 00659 series_name=series_season_ep[0] # Leave the series name alone 00660 00661 if SID == True: 00662 URLs = t.ttvdb_parseBanners(series_name) 00663 else: 00664 URLs = t.ttvdb_parseBanners(t._nameToSid(series_name)) 00665 00666 if graphics_type == fanart_type: # Series fanart graphics 00667 if not len(URLs[u'fanart']): 00668 return [] 00669 for url in URLs[u'fanart']: 00670 graphics.append(url) 00671 elif len(series_season_ep) == 1: 00672 if not len(URLs[u'series']): 00673 return [] 00674 if graphics_type == banner_type: # Series Banners 00675 for url in URLs[u'series']: 00676 graphics.append(url) 00677 else: # Series Posters 00678 for url in URLs[u'poster']: 00679 graphics.append(url) 00680 else: 00681 if not len(URLs[u'season']): 00682 return [] 00683 if graphics_type == banner_type: # Season Banners 00684 season_banners=[] 00685 for url in URLs[u'season']: 00686 if url[u'bannertype2'] == u'seasonwide' and url[u'season'] == series_season_ep[1]: 00687 season_banners.append(url) 00688 if not len(season_banners): 00689 return [] 00690 graphics = season_banners 00691 else: # Season Posters 00692 season_posters=[] 00693 for url in URLs[u'season']: 00694 if url[u'bannertype2'] == u'season' and url[u'season'] == series_season_ep[1]: 00695 season_posters.append(url) 00696 if not len(season_posters): 00697 return [] 00698 graphics = season_posters 00699 00700 graphicsURLs=[] 00701 if single_option==False: 00702 graphicsURLs.append(graphics_type+':') 00703 00704 count = 0 00705 wasanythingadded = 0 00706 anyotherlanguagegraphics=[] 00707 englishlanguagegraphics=[] 00708 for URL in graphics: 00709 if graphics_type == 'filename': 00710 if URL[graphics_type] == None: 00711 continue 00712 if language: # Is there a language to filter URLs on? 00713 if language == URL['language']: 00714 graphicsURLs.append((URL['_bannerpath']).replace(http_find, http_replace)) 00715 else: # Check for fall back graphics in case there are no selected language graphics 00716 if u'en' == URL['language']: 00717 englishlanguagegraphics.append((URL['_bannerpath']).replace(http_find, http_replace)) 00718 else: 00719 anyotherlanguagegraphics.append((URL['_bannerpath']).replace(http_find, http_replace)) 00720 else: 00721 graphicsURLs.append((URL['_bannerpath']).replace(http_find, http_replace)) 00722 if wasanythingadded == len(graphicsURLs): 00723 continue 00724 wasanythingadded = len(graphicsURLs) 00725 00726 if not len(graphicsURLs): 00727 if len(englishlanguagegraphics): # Fall back to English graphics 00728 graphicsURLs = englishlanguagegraphics 00729 elif len(anyotherlanguagegraphics): # Fall back-back to any available graphics 00730 graphicsURLs = anyotherlanguagegraphics 00731 00732 if opts.debug == True: 00733 print u"\nGraphics:\n", graphicsURLs 00734 00735 if len(graphicsURLs) == 1 and graphicsURLs[0] == graphics_type+':': 00736 return [] # Due to the language filter there may not be any URLs 00737 return(graphicsURLs) 00738 # end get_graphics 00739 00740 # Massage episode name to match those in thetvdb.com for this series 00741 def massageEpisode_name(ep_name, series_season_ep): 00742 for edit in override[series_season_ep[0].lower()][1]: 00743 ep_name=ep_name.replace(edit[0],edit[1]) # Edit episode name for each set of strings 00744 return ep_name 00745 # end massageEpisode_name 00746 00747 # Remove '|' and replace with commas 00748 def change_to_commas(meta_data): 00749 if not meta_data: return meta_data 00750 meta_data = (u'|'.join([d for d in meta_data.split('| ') if d])) 00751 return (u', '.join([d for d in meta_data.split('|') if d])) 00752 # end change_to_commas 00753 00754 # Change & values to ascii equivalents 00755 def change_amp(text): 00756 if not text: return text 00757 text = text.replace(""", "'").replace("\r\n", " ") 00758 text = text.replace(r"\'", "'") 00759 return text 00760 # end change_amp 00761 00762 # Prepare for includion into a DB 00763 def make_db_ready(text): 00764 if not text: return text 00765 text = text.replace(u'\u2013', "-") 00766 text = text.replace(u'\u2014', "-") 00767 text = text.replace(u'\u2018', "'") 00768 text = text.replace(u'\u2019', "'") 00769 text = text.replace(u'\u2026', "...") 00770 text = text.replace(u'\u201c', '"') 00771 text = text.replace(u'\u201d', '"') 00772 text = text.encode('latin-1', 'backslashreplace') 00773 return text 00774 # end make_db_ready 00775 00776 # Get Series Episode data by season 00777 def Getseries_episode_data(t, opts, series_season_ep, language = None): 00778 global screenshot_request, http_find, http_replace 00779 00780 args = len(series_season_ep) 00781 series_name='' 00782 if opts.configure != "" and override.has_key(series_season_ep[0].lower()): 00783 series_name=override[series_season_ep[0].lower()][0] # Override series name 00784 else: 00785 series_name=series_season_ep[0] # Leave the series name alone 00786 00787 # Get Cast members 00788 cast_members='' 00789 try: 00790 tmp_cast = search_for_series(t, series_name)['_actors'] 00791 except: 00792 cast_members='' 00793 if len(tmp_cast): 00794 cast_members='' 00795 for cast in tmp_cast: 00796 if cast['name']: 00797 cast_members+=(cast['name']+u', ').encode('utf8') 00798 if cast_members != '': 00799 try: 00800 cast_members = cast_members[:-2].encode('utf8') 00801 except UnicodeDecodeError: 00802 cast_members = unicode(cast_members[:-2],'utf8') 00803 cast_members = change_amp(cast_members) 00804 cast_members = change_to_commas(cast_members) 00805 cast_members=cast_members.replace('\n',' ') 00806 00807 # Get genre(s) 00808 genres='' 00809 try: 00810 genres_string = search_for_series(t, series_name)[u'genre'].encode('utf-8') 00811 except: 00812 genres_string='' 00813 if genres_string != None and genres_string != '': 00814 genres = change_amp(genres_string) 00815 genres = change_to_commas(genres) 00816 00817 seasons=search_for_series(t, series_name).keys() # Get the seasons for this series 00818 for season in seasons: 00819 if args > 1: # If a season was specified skip other seasons 00820 if season != int(series_season_ep[1]): 00821 continue 00822 episodes=search_for_series(t, series_name)[season].keys() # Get the episodes for this season 00823 for episode in episodes: # If an episode was specified skip other episodes 00824 if args > 2: 00825 if episode != int(series_season_ep[2]): 00826 continue 00827 extra_ep_data=[] 00828 available_keys=search_for_series(t, series_name)[season][episode].keys() 00829 if screenshot_request: 00830 if u'filename' in available_keys: 00831 screenshot = search_for_series(t, series_name)[season][episode][u'filename'] 00832 if screenshot: 00833 print screenshot.replace(http_find, http_replace) 00834 return 00835 else: 00836 return 00837 key_values=[] 00838 for values in data_keys: # Initialize an array for each possible data element for 00839 key_values.append('') # each episode within a season 00840 for key in available_keys: 00841 try: 00842 i = data_keys.index(key) # Include only specific episode data 00843 except ValueError: 00844 if search_for_series(t, series_name)[season][episode][key] != None: 00845 text = search_for_series(t, series_name)[season][episode][key] 00846 text = change_amp(text) 00847 text = change_to_commas(text) 00848 if text == 'None' and key.title() == 'Director': 00849 text = u"Unknown" 00850 try: 00851 extra_ep_data.append(u"%s:%s" % (key.title(), text)) 00852 except UnicodeDecodeError: 00853 extra_ep_data.append(u"%s:%s" % (key.title(), unicode(text, "utf8"))) 00854 continue 00855 text = search_for_series(t, series_name)[season][episode][key] 00856 00857 if text == None and key.title() == 'Director': 00858 text = u"Unknown" 00859 if text == None or text == 'None': 00860 continue 00861 else: 00862 text = change_amp(text) 00863 value = change_to_commas(text) 00864 value = value.replace(u'\n', u' ') 00865 key_values[i]=value 00866 index = 0 00867 if SID == False: 00868 print u"Title:%s" % series_name # Ouput the full series name 00869 else: 00870 print u"Title:%s" % search_for_series(t, series_name)[u'seriesname'] 00871 00872 for key in data_titles: 00873 if key_values[index] != None: 00874 if data_titles[index] == u'ReleaseDate:' and len(key_values[index]) > 4: 00875 print u'%s%s'% (u'Year:', key_values[index][:4]) 00876 if key_values[index] != 'None': 00877 print u'%s%s' % (data_titles[index], key_values[index]) 00878 index+=1 00879 cast_print=False 00880 for extra_data in extra_ep_data: 00881 if extra_data[:extra_data.index(':')] == u'Gueststars': 00882 extra_cast = extra_data[extra_data.index(':')+1:] 00883 if (len(extra_cast)>128) and not extra_cast.count(','): 00884 continue 00885 if cast_members: 00886 extra_data=(u"Cast:%s" % cast_members)+', '+extra_cast 00887 else: 00888 extra_data=u"Cast:%s" % extra_cast 00889 cast_print=True 00890 print extra_data 00891 if cast_print == False: 00892 print u"Cast:%s" % cast_members 00893 if genres != '': 00894 print u"Genres:%s" % genres 00895 print u"Runtime:%s" % search_for_series(t, series_name)[u'runtime'] 00896 00897 # URL to TVDB web site episode web page for this series 00898 for url_data in [u'seriesid', u'seasonid', u'id']: 00899 if not url_data in available_keys: 00900 break 00901 else: 00902 print u'URL:http://www.thetvdb.com/?tab=episode&seriesid=%s&seasonid=%s&id=%s' % (search_for_series(t, series_name)[season][episode][u'seriesid'], search_for_series(t, series_name)[season][episode][u'seasonid'],search_for_series(t, series_name)[season][episode][u'id']) 00903 # end Getseries_episode_data 00904 00905 # Get Series Season and Episode numbers 00906 def Getseries_episode_numbers(t, opts, series_season_ep): 00907 def _episode_sort(episode): 00908 seasonnumber = 0 00909 episodenumber = 0 00910 try: seasonnumber = int(episode['seasonnumber']) 00911 except: pass 00912 try: episodenumber = int(episode['episodenumber']) 00913 except: pass 00914 return (-episode.distance, seasonnumber, episodenumber) 00915 00916 global xmlFlag 00917 series_name='' 00918 ep_name='' 00919 if opts.configure != "" and override.has_key(series_season_ep[0].lower()): 00920 series_name=override[series_season_ep[0].lower()][0] # Override series name 00921 ep_name=series_season_ep[1] 00922 if len(override[series_season_ep[0].lower()][1]) != 0: # Are there search-replace strings? 00923 ep_name=massageEpisode_name(ep_name, series_season_ep) 00924 else: 00925 series_name=series_season_ep[0] # Leave the series name alone 00926 ep_name=series_season_ep[1] # Leave the episode name alone 00927 00928 season_ep_num=search_for_series(t, series_name).fuzzysearch(ep_name, 'episodename') 00929 if len(season_ep_num) != 0: 00930 for episode in sorted(season_ep_num, key=lambda ep: _episode_sort(ep), reverse=True): 00931 # if episode.distance == 0: # exact match 00932 if xmlFlag: 00933 displaySeriesXML(t, [series_name, episode['seasonnumber'], episode['episodenumber']]) 00934 sys.exit(0) 00935 print season_and_episode_num.replace('\\n', '\n') % (int(episode['seasonnumber']), int(episode['episodenumber'])) 00936 # elif (episode['episodename'].lower()).startswith(ep_name.lower()): 00937 # if len(episode['episodename']) > (len(ep_name)+1): 00938 # if episode['episodename'][len(ep_name):len(ep_name)+2] != ' (': 00939 # continue # Skip episodes the are not part of a set of (1), (2) ... etc 00940 # if xmlFlag: 00941 # displaySeriesXML(t, [series_name, episode['seasonnumber'], episode['episodenumber']]) 00942 # sys.exit(0) 00943 # print season_and_episode_num.replace('\\n', '\n') % (int(episode['seasonnumber']), int(episode['episodenumber'])) 00944 # end Getseries_episode_numbers 00945 00946 # Set up a custom interface to get all series matching a partial series name 00947 class returnAllSeriesUI(tvdb_ui.BaseUI): 00948 def __init__(self, config, log): 00949 self.config = config 00950 self.log = log 00951 00952 def selectSeries(self, allSeries): 00953 return allSeries 00954 # ends returnAllSeriesUI 00955 00956 def initialize_override_dictionary(useroptions): 00957 """ Change variables through a user supplied configuration file 00958 return False and exit the script if there are issues with the configuration file values 00959 """ 00960 if useroptions[0]=='~': 00961 useroptions=os.path.expanduser("~")+useroptions[1:] 00962 if os.path.isfile(useroptions) == False: 00963 sys.stderr.write( 00964 "! The specified user configuration file (%s) is not a file\n" % useroptions 00965 ) 00966 sys.exit(1) 00967 massage = {} 00968 overrides = {} 00969 cfg = ConfigParser.SafeConfigParser() 00970 cfg.read(useroptions) 00971 00972 for section in cfg.sections(): 00973 if section == 'regex': 00974 # Change variables per user config file 00975 for option in cfg.options(section): 00976 name_parse.append(re.compile(cfg.get(section, option))) 00977 continue 00978 if section =='ep_name_massage': 00979 for option in cfg.options(section): 00980 tmp =cfg.get(section, option).split(',') 00981 if len(tmp)%2 and len(cfg.get(section, option)) != 0: 00982 sys.stderr.write("! For (%s) 'ep_name_massage' values must be in pairs\n" % option) 00983 sys.exit(1) 00984 tmp_array=[] 00985 i=0 00986 while i != len(tmp): 00987 tmp_array.append([tmp[i].replace('"',''), tmp[i+1].replace('"','')]) 00988 i+=2 00989 massage[option]=tmp_array 00990 continue 00991 if section =='series_name_override': 00992 for option in cfg.options(section): 00993 overrides[option] = cfg.get(section, option) 00994 tvdb = Tvdb(banners=False, debug = False, interactive = False, cache = cache_dir, custom_ui=returnAllSeriesUI, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV 00995 for key in overrides.keys(): 00996 sid = overrides[key] 00997 if len(sid) == 0: 00998 continue 00999 try: # Check that the SID (Series id) is numeric 01000 dummy = int(sid) 01001 except: 01002 sys.stdout.write("! Series (%s) Invalid SID (not numeric) [%s] in config file\n" % (key, sid)) 01003 sys.exit(1) 01004 # Make sure that the series name is not empty or all blanks 01005 if len(key.replace(' ','')) == 0: 01006 sys.stdout.write("! Invalid Series name (must have some non-blank characters) [%s] in config file\n" % key) 01007 print parts 01008 sys.exit(1) 01009 01010 try: 01011 series_name_sid=tvdb.series_by_sid(sid) 01012 except: 01013 sys.stdout.write("! Invalid Series (no matches found in thetvdb,com) (%s) sid (%s) in config file\n" % (key, sid)) 01014 sys.exit(1) 01015 overrides[key]=series_name_sid[u'seriesname'].encode('utf-8') 01016 continue 01017 01018 for key in overrides.keys(): 01019 override[key] = [overrides[key],[]] 01020 01021 for key in massage.keys(): 01022 if override.has_key(key): 01023 override[key][1]=massage[key] 01024 else: 01025 override[key]=[key, massage[key]] 01026 return 01027 # END initialize_override_dictionary 01028 01029 def initializeXslt(language): 01030 ''' Initalize all data and functions for XSLT stylesheet processing 01031 return nothing 01032 ''' 01033 global xslt, tvdbXpath 01034 try: 01035 import MythTV.ttvdb.tvdbXslt as tvdbXslt 01036 except Exception, errmsg: 01037 sys.stderr.write('! Error: Importing tvdbXslt error(%s)\n' % errmsg) 01038 sys.exit(1) 01039 01040 xslt = tvdbXslt.xpathFunctions() 01041 xslt.language = language 01042 xslt.buildFuncDict() 01043 tvdbXpath = etree.FunctionNamespace('http://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format') 01044 tvdbXpath.prefix = 'tvdbXpath' 01045 for key in xslt.FuncDict.keys(): 01046 tvdbXpath[key] = xslt.FuncDict[key] 01047 return 01048 # end initializeXslt() 01049 01050 def displaySearchXML(tvdb_api): 01051 '''Using a XSLT style sheet translate TVDB search results into the MythTV Universal Query format 01052 return nothing 01053 ''' 01054 global xslt, tvdbXpath 01055 01056 # Remove duplicates when a non-English language code is specified 01057 if xslt.language != 'en': 01058 compareFilter = etree.XPath('//id[text()=$Id]') 01059 idLangFilter = etree.XPath('//id[text()=$Id]/../language[text()="en"]/..') 01060 tmpTree = deepcopy(tvdb_api.searchTree) 01061 for seriesId in tmpTree.xpath('//Series/id/text()'): 01062 if len(compareFilter(tvdb_api.searchTree, Id=seriesId)) > 1: 01063 tmpList = idLangFilter(tvdb_api.searchTree, Id=seriesId) 01064 if len(tmpList): 01065 tvdb_api.searchTree.remove(tmpList[0]) 01066 01067 tvdbQueryXslt = etree.XSLT(etree.parse(u'%s%s' % (tvdb_api.baseXsltDir, u'tvdbQuery.xsl'))) 01068 items = tvdbQueryXslt(tvdb_api.searchTree) 01069 if items.getroot() != None: 01070 if len(items.xpath('//item')): 01071 sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) 01072 sys.exit(0) 01073 # end displaySearchXML() 01074 01075 def displaySeriesXML(tvdb_api, series_season_ep): 01076 '''Using a XSLT style sheet translate TVDB Series data results into the 01077 MythTV Universal Query format 01078 return nothing 01079 ''' 01080 global xslt, tvdbXpath 01081 allDataElement = etree.XML(u'<allData></allData>') 01082 requestDetails = etree.XML(u'<requestDetails></requestDetails>') 01083 requestDetails.attrib['lang'] = xslt.language 01084 requestDetails.attrib['series'] = series_season_ep[0] 01085 requestDetails.attrib['season'] = series_season_ep[1] 01086 requestDetails.attrib['episode'] = series_season_ep[2] 01087 allDataElement.append(requestDetails) 01088 01089 # Combine the various XML inputs into a single XML element and send to the XSLT stylesheet 01090 if tvdb_api.epInfoTree != None: 01091 allDataElement.append(tvdb_api.epInfoTree) 01092 else: 01093 sys.exit(0) 01094 if tvdb_api.actorsInfoTree != None: 01095 allDataElement.append(tvdb_api.actorsInfoTree) 01096 else: 01097 allDataElement.append(etree.XML(u'<Actors></Actors>')) 01098 if tvdb_api.imagesInfoTree != None: 01099 allDataElement.append(tvdb_api.imagesInfoTree) 01100 else: 01101 allDataElement.append(etree.XML(u'<Banners></Banners>')) 01102 01103 tvdbQueryXslt = etree.XSLT(etree.parse(u'%s%s' % (tvdb_api.baseXsltDir, u'tvdbVideo.xsl'))) 01104 items = tvdbQueryXslt(allDataElement) 01105 if items.getroot() != None: 01106 if len(items.xpath('//item')): 01107 sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) 01108 sys.exit(0) 01109 # end displaySeriesXML() 01110 01111 def displayCollectionXML(tvdb_api): 01112 '''Using a XSLT style sheet translate TVDB series results into the MythTV Universal Query format 01113 return nothing 01114 ''' 01115 global xslt, tvdbXpath 01116 01117 # Remove duplicates when non-English language code is specified 01118 if xslt.language != 'en': 01119 compareFilter = etree.XPath('//id[text()=$Id]') 01120 idLangFilter = etree.XPath('//id[text()=$Id]/../language[text()="en"]/..') 01121 tmpTree = deepcopy(tvdb_api.seriesInfoTree) 01122 for seriesId in tmpTree.xpath('//Series/id/text()'): 01123 if len(compareFilter(tvdb_api.seriesInfoTree, Id=seriesId)) > 1: 01124 tmpList = idLangFilter(tvdb_api.seriesInfoTree, Id=seriesId) 01125 if len(tmpList): 01126 tvdb_api.seriesInfoTree.remove(tmpList[0]) 01127 01128 tvdbCollectionXslt = etree.XSLT(etree.parse(u'%s%s' % (tvdb_api.baseXsltDir, u'tvdbCollection.xsl'))) 01129 items = tvdbCollectionXslt(tvdb_api.seriesInfoTree) 01130 if items.getroot() != None: 01131 if len(items.xpath('//item')): 01132 sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) 01133 sys.exit(0) 01134 # end displayCollectionXML() 01135 01136 def main(): 01137 parser = OptionParser(usage=u"%prog usage: ttvdb -hdruviomMPFBDS [parameters]\n <series name or 'series and season number' or 'series and season number and episode number'>\n\nFor details on using ttvdb with Mythvideo see the ttvdb wiki page at:\nhttp://www.mythtv.org/wiki/Ttvdb.py") 01138 01139 parser.add_option( "-d", "--debug", action="store_true", default=False, dest="debug", 01140 help=u"Show debugging info") 01141 parser.add_option( "-r", "--raw", action="store_true",default=False, dest="raw", 01142 help=u"Dump raw data only") 01143 parser.add_option( "-u", "--usage", action="store_true", default=False, dest="usage", 01144 help=u"Display examples for executing the ttvdb script") 01145 parser.add_option( "-v", "--version", action="store_true", default=False, dest="version", 01146 help=u"Display version and author") 01147 parser.add_option( "-i", "--interactive", action="store_true", default=False, dest="interactive", 01148 help=u"Interaction mode (allows selection of a specific Series)") 01149 parser.add_option( "-c", "--configure", metavar="FILE", default="", dest="configure", 01150 help=u"Use configuration settings") 01151 parser.add_option( "-l", "--language", metavar="LANGUAGE", default=u'en', dest="language", 01152 help=u"Select data that matches the specified language fall back to english if nothing found (e.g. 'es' Español, 'de' Deutsch ... etc)") 01153 parser.add_option( "-n", "--num_seasons", action="store_true", default=False, dest="num_seasons", 01154 help=u"Return the season numbers for a series") 01155 parser.add_option( "-t", action="store_true", default=False, dest="test", 01156 help=u"Test for the availability of runtime dependencies") 01157 parser.add_option( "-m", "--mythvideo", action="store_true", default=False, dest="mythvideo", 01158 help=u"Conform to mythvideo standards when processing -M, -P, -F and -D") 01159 parser.add_option( "-M", "--list", action="store_true", default=False, dest="list", 01160 help=u"Get matching TV Series list") 01161 parser.add_option( "-P", "--poster", action="store_true", default=False, dest="poster", 01162 help=u"Get Series Poster URL(s)") 01163 parser.add_option( "-F", "--fanart", action="store_true", default=False, dest="fanart", 01164 help=u"Get Series fan art URL(s)") 01165 parser.add_option( "-B", "--backdrop", action="store_true", default=False, dest="banner", 01166 help=u"Get Series banner/backdrop URL(s)") 01167 parser.add_option( "-S", "--screenshot", action="store_true", default=False, dest="screenshot", 01168 help=u"Get Series episode screenshot URL") 01169 parser.add_option( "-D", "--data", action="store_true", default=False, dest="data", 01170 help=u"Get Series episode data") 01171 parser.add_option( "-N", "--numbers", action="store_true", default=False, dest="numbers", 01172 help=u"Get Season and Episode numbers") 01173 parser.add_option( "-C", "--collection", action="store_true", default=False, dest="collection", 01174 help=u'Get a TV Series (collection) "series" level information') 01175 01176 opts, series_season_ep = parser.parse_args() 01177 01178 01179 # Test mode, if we've made it here, everything is ok 01180 if opts.test: 01181 print "Everything appears to be in order" 01182 sys.exit(0) 01183 01184 # Make everything unicode utf8 01185 for index in range(len(series_season_ep)): 01186 series_season_ep[index] = unicode(series_season_ep[index], 'utf8') 01187 01188 if opts.debug == True: 01189 print "opts", opts 01190 print "\nargs", series_season_ep 01191 01192 # Process version command line requests 01193 if opts.version == True: 01194 version = etree.XML(u'<grabber></grabber>') 01195 etree.SubElement(version, "name").text = __title__ 01196 etree.SubElement(version, "author").text = __author__ 01197 etree.SubElement(version, "thumbnail").text = 'ttvdb.png' 01198 etree.SubElement(version, "command").text = 'ttvdb.py' 01199 etree.SubElement(version, "type").text = 'television' 01200 etree.SubElement(version, "description").text = 'Search and metadata downloads for thetvdb.com' 01201 etree.SubElement(version, "version").text = __version__ 01202 sys.stdout.write(etree.tostring(version, encoding='UTF-8', pretty_print=True)) 01203 sys.exit(0) 01204 01205 # Process usage command line requests 01206 if opts.usage == True: 01207 sys.stdout.write(usage_txt) 01208 sys.exit(0) 01209 01210 if len(series_season_ep) == 0: 01211 parser.error("! No series or series season episode supplied") 01212 sys.exit(1) 01213 01214 # Default output format of season and episode numbers 01215 global season_and_episode_num, screenshot_request 01216 season_and_episode_num='S%02dE%02d' # Format output example "S04E12" 01217 01218 if opts.numbers == False: 01219 if len(series_season_ep) > 1: 01220 if not _can_int(series_season_ep[1]): 01221 parser.error("! Season is not numeric") 01222 sys.exit(1) 01223 if len(series_season_ep) > 2: 01224 if not _can_int(series_season_ep[2]): 01225 parser.error("! Episode is not numeric") 01226 sys.exit(1) 01227 else: 01228 if len(series_season_ep) < 2: 01229 parser.error("! An Episode name must be included") 01230 sys.exit(1) 01231 if len(series_season_ep) == 3: 01232 season_and_episode_num = series_season_ep[2] # Override default output format 01233 01234 if opts.screenshot: 01235 if len(series_season_ep) > 1: 01236 if not _can_int(series_season_ep[1]): 01237 parser.error("! Season is not numeric") 01238 sys.exit(1) 01239 if len(series_season_ep) > 2: 01240 if not _can_int(series_season_ep[2]): 01241 parser.error("! Episode is not numeric") 01242 sys.exit(1) 01243 if not len(series_season_ep) > 2: 01244 parser.error("! Option (-S), episode screenshot search requires Season and Episode numbers") 01245 sys.exit(1) 01246 screenshot_request = True 01247 01248 if opts.debug == True: 01249 print series_season_ep 01250 01251 if opts.debug == True: 01252 print "#"*20 01253 print "# series_season_ep array(",series_season_ep,")" 01254 01255 if opts.debug == True: 01256 print "#"*20 01257 print "# Starting tvtvb" 01258 print "# Processing (%s) Series" % ( series_season_ep[0] ) 01259 01260 # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml 01261 # Hard-coded here as it is realtively static, and saves another HTTP request, as 01262 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml 01263 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"] 01264 01265 # Validate language as specified by user 01266 if not opts.language in valid_languages: 01267 opts.language = 'en' # Set the default to English when an invalid language was specified 01268 01269 # Set XML to be the default display mode for -N, -M, -D, -C 01270 opts.xml = True 01271 initializeXslt(opts.language) 01272 01273 # Access thetvdb.com API with banners (Posters, Fanart, banners, screenshots) data retrieval enabled 01274 if opts.list ==True: 01275 t = Tvdb(banners=False, debug = opts.debug, cache = cache_dir, custom_ui=returnAllSeriesUI, language = opts.language, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV 01276 if opts.xml: 01277 t.xml = True 01278 elif opts.interactive == True: 01279 t = Tvdb(banners=True, debug=opts.debug, interactive=True, select_first=False, cache=cache_dir, actors = True, language = opts.language, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV 01280 if opts.xml: 01281 t.xml = True 01282 else: 01283 t = Tvdb(banners=True, debug = opts.debug, cache = cache_dir, actors = True, language = opts.language, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV 01284 if opts.xml: 01285 t.xml = True 01286 01287 # Determine if there is a SID or a series name to search with 01288 global SID 01289 SID = False 01290 if _can_int(series_season_ep[0]): # if it is numeric then assume it is a series ID number 01291 SID = True 01292 else: 01293 SID = False 01294 01295 # The -C collections options only supports a SID as input 01296 if opts.collection: 01297 if SID: 01298 pass 01299 else: 01300 parser.error("! Option (-C), collection requires an inetref number") 01301 sys.exit(1) 01302 01303 if opts.debug == True: 01304 print "# ..got tvdb mirrors" 01305 print "# Start to process series or series_season_ep" 01306 print "#"*20 01307 01308 global override 01309 override={} # Initialize series name override dictionary 01310 # If the user wants Series name overrides and a override file exists then create an overide dictionary 01311 if opts.configure != '': # Did the user want to override the default config file name/location 01312 if opts.configure[0]=='~': 01313 opts.configure=os.path.expanduser("~")+opts.configure[1:] 01314 if os.path.exists(opts.configure) == 1: # Do overrides exist? 01315 initialize_override_dictionary(opts.configure) 01316 else: 01317 debuglog("! The specified override file (%s) does not exist" % opts.configure) 01318 sys.exit(1) 01319 else: # Check if there is a default configuration file 01320 default_config = u"%s/%s" % (os.path.expanduser(u"~"), u".mythtv/ttvdb.conf") 01321 if os.path.isfile(default_config): 01322 opts.configure = default_config 01323 initialize_override_dictionary(opts.configure) 01324 01325 if len(override) == 0: 01326 opts.configure = False # Turn off the override option as there is nothing to override 01327 01328 # Check if a video name was passed and if so parse it for series name, season and episode numbers 01329 if not opts.collection and len(series_season_ep) == 1: 01330 for r in name_parse: 01331 match = r.match(series_season_ep[0]) 01332 if match: 01333 seriesname, seasno, epno = match.groups() 01334 #remove ._- characters from name (- removed only if next to end of line) 01335 seriesname = re.sub("[\._]|\-(?=$)", " ", seriesname).strip() 01336 series_season_ep = [seriesname, seasno, epno] 01337 break # Matched - to the next file! 01338 01339 # Fetch a list of matching series names 01340 if opts.list ==True: 01341 try: 01342 allSeries=t._getSeries(series_season_ep[0]) 01343 except tvdb_shownotfound: 01344 sys.exit(0) # No matching series 01345 except Exception, e: 01346 sys.stderr.write("! Error: %s\n" % (e)) 01347 sys.exit(1) # Most likely a communications error 01348 if opts.xml: 01349 displaySearchXML(t) 01350 sys.exit(0) 01351 match_list = [] 01352 for series_name_sid in allSeries: # list search results 01353 key_value = u"%s:%s" % (series_name_sid['sid'], series_name_sid['name']) 01354 if not key_value in match_list: # Do not add duplicates 01355 match_list.append(key_value) 01356 print key_value 01357 sys.exit(0) # The Series list option (-M) is the only option honoured when used 01358 01359 # Fetch TV series collection information 01360 if opts.collection: 01361 try: 01362 t._getShowData(series_season_ep[0]) 01363 except tvdb_shownotfound: 01364 sys.exit(0) # No matching series 01365 except Exception, e: 01366 sys.stderr.write("! Error: %s\n" % (e)) 01367 sys.exit(1) # Most likely a communications error 01368 displayCollectionXML(t) 01369 sys.exit(0) # The TV Series collection option (-C) is the only option honoured when used 01370 01371 # Verify that thetvdb.com has the desired series_season_ep. 01372 # Exit this module if series_season_ep is not found 01373 if opts.numbers == False and opts.num_seasons == False: 01374 seriesfound=searchseries(t, opts, series_season_ep) 01375 x=1 01376 else: 01377 x=[] 01378 x.append(series_season_ep[0]) # Only use series name in check 01379 seriesfound=searchseries(t, opts, x) 01380 01381 # Return the season numbers for a series 01382 if opts.num_seasons == True: 01383 season_numbers='' 01384 for x in seriesfound.keys(): 01385 season_numbers+='%d,' % x 01386 print season_numbers[:-1] 01387 sys.exit(0) # Option (-n) is the only option honoured when used 01388 01389 # Dump information accessable for a Series and ONLY first season of episoded data 01390 if opts.debug == True: 01391 print "#"*20 01392 print "# Starting Raw keys call" 01393 print "Lvl #1:" # Seasons for series 01394 x = t[series_season_ep[0]].keys() 01395 print t[series_season_ep[0]].keys() 01396 print "#"*20 01397 print "Lvl #2:" # Episodes for each season 01398 for y in x: 01399 print t[series_season_ep[0]][y].keys() 01400 print "#"*20 01401 print "Lvl #3:" # Keys for each episode within the 1st season 01402 z = t[series_season_ep[0]][1].keys() 01403 for aa in z: 01404 print t[series_season_ep[0]][1][aa].keys() 01405 print "#"*20 01406 print "Lvl #4:" # Available data for each episode in 1st season 01407 for aa in z: 01408 codes = t[series_season_ep[0]][1][aa].keys() 01409 print "\n\nStart:" 01410 for c in codes: 01411 print "="*50 01412 print 'Key Name=('+c+'):' 01413 print t[series_season_ep[0]][1][aa][c] 01414 print "="*50 01415 print "#"*20 01416 sys.exit (True) 01417 01418 if opts.numbers == True: # Fetch and output season and episode numbers 01419 global xmlFlag 01420 if opts.xml: 01421 xmlFlag = True 01422 else: 01423 xmlFlag = False 01424 Getseries_episode_numbers(t, opts, series_season_ep) 01425 sys.exit(0) # The Numbers option (-N) is the only option honoured when used 01426 01427 if opts.data or screenshot_request: # Fetch and output episode data 01428 if opts.mythvideo: 01429 if len(series_season_ep) != 3: 01430 print u"Season and Episode numbers required." 01431 else: 01432 if opts.xml: 01433 displaySeriesXML(t, series_season_ep) 01434 sys.exit(0) 01435 Getseries_episode_data(t, opts, series_season_ep, language=opts.language) 01436 else: 01437 if opts.xml and len(series_season_ep) == 3: 01438 displaySeriesXML(t, series_season_ep) 01439 sys.exit(0) 01440 Getseries_episode_data(t, opts, series_season_ep, language=opts.language) 01441 01442 # Fetch the requested graphics URL(s) 01443 if opts.debug == True: 01444 print "#"*20 01445 print "# Checking if Posters, Fanart or Banners are available" 01446 print "#"*20 01447 01448 if opts.configure != "" and override.has_key(series_season_ep[0].lower()): 01449 banners_keys = search_for_series(t, override[series_season_ep[0].lower()][0])['_banners'].keys() 01450 else: 01451 banners_keys = search_for_series(t, series_season_ep[0])['_banners'].keys() 01452 01453 banner= False 01454 poster= False 01455 fanart= False 01456 01457 for x in banners_keys: # Determine what type of graphics is available 01458 if x == fanart_key: 01459 fanart=True 01460 elif x== poster_key: 01461 poster=True 01462 elif x==season_key or x==banner_key: 01463 banner=True 01464 01465 # Make sure that some graphics URL(s) (Posters, FanArt or Banners) are available 01466 if ( fanart!=True and poster!=True and banner!=True ): 01467 sys.exit(0) 01468 01469 if opts.debug == True: 01470 print "#"*20 01471 print "# One or more of Posters, Fanart or Banners are available" 01472 print "#"*20 01473 01474 # Determine if graphic URL identification output is required 01475 if opts.data: # Along with episode data get all graphics 01476 opts.poster = True 01477 opts.fanart = True 01478 opts.banner = True 01479 single_option = True 01480 fanart, banner, poster = (True, True, True) 01481 else: 01482 y=0 01483 single_option=True 01484 if opts.poster==True: 01485 y+=1 01486 if opts.fanart==True: 01487 y+=1 01488 if opts.banner==True: 01489 y+=1 01490 01491 if (poster==True and opts.poster==True and opts.raw!=True): # Get posters and send to stdout 01492 season_poster_found = False 01493 if opts.mythvideo: 01494 if len(series_season_ep) < 2: 01495 print u"Season and Episode numbers required." 01496 sys.exit(0) 01497 all_posters = u'Coverart:' 01498 all_empty = len(all_posters) 01499 for p in get_graphics(t, opts, series_season_ep, poster_type, single_option, opts.language): 01500 all_posters = all_posters+p+u',' 01501 season_poster_found = True 01502 if season_poster_found == False: # If there were no season posters get the series top poster 01503 series_name='' 01504 if opts.configure != "" and override.has_key(series_season_ep[0].lower()): 01505 series_name=override[series_season_ep[0].lower()][0] # Override series name 01506 else: 01507 series_name=series_season_ep[0] # Leave the series name alone 01508 for p in get_graphics(t, opts, [series_name], poster_type, single_option, opts.language): 01509 all_posters = all_posters+p+u',' 01510 if len(all_posters) > all_empty: 01511 if all_posters[-1] == u',': 01512 print all_posters[:-1] 01513 else: 01514 print all_posters 01515 01516 if (fanart==True and opts.fanart==True and opts.raw!=True): # Get Fan Art and send to stdout 01517 all_fanart = u'Fanart:' 01518 all_empty = len(all_fanart) 01519 for f in get_graphics(t, opts, series_season_ep, fanart_type, single_option, opts.language): 01520 all_fanart = all_fanart+f+u',' 01521 if len(all_fanart) > all_empty: 01522 if all_fanart[-1] == u',': 01523 print all_fanart[:-1] 01524 else: 01525 print all_fanart 01526 01527 if (banner==True and opts.banner==True and opts.raw!=True): # Also change to get ALL Series graphics 01528 season_banner_found = False 01529 if opts.mythvideo: 01530 if len(series_season_ep) < 2: 01531 print u"Season and Episode numbers required." 01532 sys.exit(0) 01533 all_banners = u'Banner:' 01534 all_empty = len(all_banners) 01535 for b in get_graphics(t, opts, series_season_ep, banner_type, single_option, opts.language): 01536 all_banners = all_banners+b+u',' 01537 season_banner_found = True 01538 if not season_banner_found: # If there were no season banner get the series top banner 01539 series_name='' 01540 if opts.configure != "" and override.has_key(series_season_ep[0].lower()): 01541 series_name=override[series_season_ep[0].lower()][0] # Override series name 01542 else: 01543 series_name=series_season_ep[0] # Leave the series name alone 01544 for b in get_graphics(t, opts, [series_name], banner_type, single_option, opts.language): 01545 all_banners = all_banners+b+u',' 01546 if len(all_banners) > all_empty: 01547 if all_banners[-1] == u',': 01548 print all_banners[:-1] 01549 else: 01550 print all_banners 01551 01552 if opts.debug == True: 01553 print "#"*20 01554 print "# Processing complete" 01555 print "#"*20 01556 sys.exit(0) 01557 #end main 01558 01559 if __name__ == "__main__": 01560 main()
1.7.6.1