· 6 years ago · Nov 27, 2019, 07:00 PM
1#!/usr/bin/env python
2#encoding:utf-8
3#author:dbr/Ben
4#project:tvdb_api
5#repository:http://github.com/dbr/tvdb_api
6#license:unlicense (http://unlicense.org/)
7
8"""Simple-to-use Python interface to The TVDB's API (thetvdb.com)
9
10Example usage:
11
12>>> from tvdb_api import Tvdb
13>>> t = Tvdb()
14>>> t['Lost'][4][11]['episodename']
15u'Cabin Fever'
16"""
17__author__ = "dbr/Ben"
18__version__ = "1.9"
19
20import os
21import time
22import urllib
23import urllib2
24import getpass
25import StringIO
26import tempfile
27import warnings
28import logging
29import datetime
30import zipfile
31
32try:
33 import xml.etree.cElementTree as ElementTree
34except ImportError:
35 import xml.etree.ElementTree as ElementTree
36
37try:
38 import gzip
39except ImportError:
40 gzip = None
41
42
43from tvdb_cache import CacheHandler
44
45from tvdb_ui import BaseUI, ConsoleUI
46from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
47 tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
48
49lastTimeout = None
50
51def log():
52 return logging.getLogger("tvdb_api")
53
54
55class ShowContainer(dict):
56 """Simple dict that holds a series of Show instances
57 """
58
59 def __init__(self):
60 self._stack = []
61 self._lastgc = time.time()
62
63 def __setitem__(self, key, value):
64 self._stack.append(key)
65
66 #keep only the 100th latest results
67 if time.time() - self._lastgc > 20:
68 tbd = self._stack[:-100]
69 i = 0
70 for o in tbd:
71 del self[o]
72 del self._stack[i]
73 i += 1
74
75 _lastgc = time.time()
76 del tbd
77
78 super(ShowContainer, self).__setitem__(key, value)
79
80
81class Show(dict):
82 """Holds a dict of seasons, and show data.
83 """
84 def __init__(self):
85 dict.__init__(self)
86 self.data = {}
87
88 def __repr__(self):
89 return "<Show %s (containing %s seasons)>" % (
90 self.data.get(u'seriesname', 'instance'),
91 len(self)
92 )
93
94 def __getitem__(self, key):
95 if key in self:
96 # Key is an episode, return it
97 return dict.__getitem__(self, key)
98
99 if key in self.data:
100 # Non-numeric request is for show-data
101 return dict.__getitem__(self.data, key)
102
103 # Data wasn't found, raise appropriate error
104 if isinstance(key, int) or key.isdigit():
105 # Episode number x was not found
106 raise tvdb_seasonnotfound("Could not find season %s" % (repr(key)))
107 else:
108 # If it's not numeric, it must be an attribute name, which
109 # doesn't exist, so attribute error.
110 raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
111
112 def airedOn(self, date):
113 ret = self.search(str(date), 'firstaired')
114 if len(ret) == 0:
115 raise tvdb_episodenotfound("Could not find any episodes that aired on %s" % date)
116 return ret
117
118 def search(self, term = None, key = None):
119 """
120 Search all episodes in show. Can search all data, or a specific key (for
121 example, episodename)
122
123 Always returns an array (can be empty). First index contains the first
124 match, and so on.
125
126 Each array index is an Episode() instance, so doing
127 search_results[0]['episodename'] will retrieve the episode name of the
128 first match.
129
130 Search terms are converted to lower case (unicode) strings.
131
132 # Examples
133
134 These examples assume t is an instance of Tvdb():
135
136 >>> t = Tvdb()
137 >>>
138
139 To search for all episodes of Scrubs with a bit of data
140 containing "my first day":
141
142 >>> t['Scrubs'].search("my first day")
143 [<Episode 01x01 - My First Day>]
144 >>>
145
146 Search for "My Name Is Earl" episode named "Faked His Own Death":
147
148 >>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
149 [<Episode 01x04 - Faked His Own Death>]
150 >>>
151
152 To search Scrubs for all episodes with "mentor" in the episode name:
153
154 >>> t['scrubs'].search('mentor', key = 'episodename')
155 [<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
156 >>>
157
158 # Using search results
159
160 >>> results = t['Scrubs'].search("my first")
161 >>> print results[0]['episodename']
162 My First Day
163 >>> for x in results: print x['episodename']
164 My First Day
165 My First Step
166 My First Kill
167 >>>
168 """
169 results = []
170 for cur_season in self.values():
171 searchresult = cur_season.search(term = term, key = key)
172 if len(searchresult) != 0:
173 results.extend(searchresult)
174
175 return results
176
177
178class Season(dict):
179 def __init__(self, show = None):
180 """The show attribute points to the parent show
181 """
182 self.show = show
183
184 def __repr__(self):
185 return "<Season instance (containing %s episodes)>" % (
186 len(self.keys())
187 )
188
189 def __getitem__(self, episode_number):
190 if episode_number not in self:
191 raise tvdb_episodenotfound("Could not find episode %s" % (repr(episode_number)))
192 else:
193 return dict.__getitem__(self, episode_number)
194
195 def search(self, term = None, key = None):
196 """Search all episodes in season, returns a list of matching Episode
197 instances.
198
199 >>> t = Tvdb()
200 >>> t['scrubs'][1].search('first day')
201 [<Episode 01x01 - My First Day>]
202 >>>
203
204 See Show.search documentation for further information on search
205 """
206 results = []
207 for ep in self.values():
208 searchresult = ep.search(term = term, key = key)
209 if searchresult is not None:
210 results.append(
211 searchresult
212 )
213 return results
214
215
216class Episode(dict):
217 def __init__(self, season = None):
218 """The season attribute points to the parent season
219 """
220 self.season = season
221
222 def __repr__(self):
223 seasno = int(self.get(u'seasonnumber', 0))
224 epno = int(self.get(u'episodenumber', 0))
225 epname = self.get(u'episodename')
226 if epname is not None:
227 return "<Episode %02dx%02d - %s>" % (seasno, epno, epname)
228 else:
229 return "<Episode %02dx%02d>" % (seasno, epno)
230
231 def __getitem__(self, key):
232 try:
233 return dict.__getitem__(self, key)
234 except KeyError:
235 raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
236
237 def search(self, term = None, key = None):
238 """Search episode data for term, if it matches, return the Episode (self).
239 The key parameter can be used to limit the search to a specific element,
240 for example, episodename.
241
242 This primarily for use use by Show.search and Season.search. See
243 Show.search for further information on search
244
245 Simple example:
246
247 >>> e = Episode()
248 >>> e['episodename'] = "An Example"
249 >>> e.search("examp")
250 <Episode 00x00 - An Example>
251 >>>
252
253 Limiting by key:
254
255 >>> e.search("examp", key = "episodename")
256 <Episode 00x00 - An Example>
257 >>>
258 """
259 if term == None:
260 raise TypeError("must supply string to search for (contents)")
261
262 term = unicode(term).lower()
263 for cur_key, cur_value in self.items():
264 cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
265 if key is not None and cur_key != key:
266 # Do not search this key
267 continue
268 if cur_value.find( unicode(term).lower() ) > -1:
269 return self
270
271
272class Actors(list):
273 """Holds all Actor instances for a show
274 """
275 pass
276
277
278class Actor(dict):
279 """Represents a single actor. Should contain..
280
281 id,
282 image,
283 name,
284 role,
285 sortorder
286 """
287 def __repr__(self):
288 return "<Actor \"%s\">" % (self.get("name"))
289
290
291class Tvdb:
292 """Create easy-to-use interface to name of season/episode name
293 >>> t = Tvdb()
294 >>> t['Scrubs'][1][24]['episodename']
295 u'My Last Day'
296 """
297 def __init__(self,
298 interactive = False,
299 select_first = False,
300 debug = False,
301 cache = True,
302 banners = False,
303 actors = False,
304 custom_ui = None,
305 language = None,
306 search_all_languages = False,
307 apikey = None,
308 forceConnect=False,
309 useZip=False,
310 dvdorder=False):
311
312 """interactive (True/False):
313 When True, uses built-in console UI is used to select the correct show.
314 When False, the first search result is used.
315
316 select_first (True/False):
317 Automatically selects the first series search result (rather
318 than showing the user a list of more than one series).
319 Is overridden by interactive = False, or specifying a custom_ui
320
321 debug (True/False) DEPRECATED:
322 Replaced with proper use of logging module. To show debug messages:
323
324 >>> import logging
325 >>> logging.basicConfig(level = logging.DEBUG)
326
327 cache (True/False/str/unicode/urllib2 opener):
328 Retrieved XML are persisted to to disc. If true, stores in
329 tvdb_api folder under your systems TEMP_DIR, if set to
330 str/unicode instance it will use this as the cache
331 location. If False, disables caching. Can also be passed
332 an arbitrary Python object, which is used as a urllib2
333 opener, which should be created by urllib2.build_opener
334
335 banners (True/False):
336 Retrieves the banners for a show. These are accessed
337 via the _banners key of a Show(), for example:
338
339 >>> Tvdb(banners=True)['scrubs']['_banners'].keys()
340 ['fanart', 'poster', 'series', 'season']
341
342 actors (True/False):
343 Retrieves a list of the actors for a show. These are accessed
344 via the _actors key of a Show(), for example:
345
346 >>> t = Tvdb(actors=True)
347 >>> t['scrubs']['_actors'][0]['name']
348 u'Zach Braff'
349
350 custom_ui (tvdb_ui.BaseUI subclass):
351 A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
352
353 language (2 character language abbreviation):
354 The language of the returned data. Is also the language search
355 uses. Default is "en" (English). For full list, run..
356
357 >>> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS
358 ['da', 'fi', 'nl', ...]
359
360 search_all_languages (True/False):
361 By default, Tvdb will only search in the language specified using
362 the language option. When this is True, it will search for the
363 show in and language
364
365 apikey (str/unicode): ****MY API HERE *****
366 Override the default thetvdb.com API key. By default it will use
367 tvdb_api's own key (fine for small scripts), but you can use your
368 own key if desired - this is recommended if you are embedding
369 tvdb_api in a larger application)
370 See http://thetvdb.com/?tab=apiregister to get your own key
371
372 forceConnect (bool):
373 If true it will always try to connect to theTVDB.com even if we
374 recently timed out. By default it will wait one minute before
375 trying again, and any requests within that one minute window will
376 return an exception immediately.
377
378 useZip (bool):
379 Download the zip archive where possibale, instead of the xml.
380 This is only used when all episodes are pulled.
381 And only the main language xml is used, the actor and banner xml are lost.
382 """
383
384 global lastTimeout
385
386 # if we're given a lastTimeout that is less than 1 min just give up
387 if not forceConnect and lastTimeout != None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1):
388 raise tvdb_error("We recently timed out, so giving up early this time")
389
390 self.shows = ShowContainer() # Holds all Show classes
391 self.corrections = {} # Holds show-name to show_id mapping
392
393 self.config = {}
394
395 if apikey is not None:
396 self.config['apikey'] = apikey
397 else:
398 self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key
399
400 self.config['debug_enabled'] = debug # show debugging messages
401
402 self.config['custom_ui'] = custom_ui
403
404 self.config['interactive'] = interactive # prompt for correct series?
405
406 self.config['select_first'] = select_first
407
408 self.config['search_all_languages'] = search_all_languages
409
410 self.config['useZip'] = useZip
411
412 self.config['dvdorder'] = dvdorder
413
414 if cache is True:
415 self.config['cache_enabled'] = True
416 self.config['cache_location'] = self._getTempDir()
417 self.urlopener = urllib2.build_opener(
418 CacheHandler(self.config['cache_location'])
419 )
420
421 elif cache is False:
422 self.config['cache_enabled'] = False
423 self.urlopener = urllib2.build_opener() # default opener with no caching
424
425 elif isinstance(cache, basestring):
426 self.config['cache_enabled'] = True
427 self.config['cache_location'] = cache
428 self.urlopener = urllib2.build_opener(
429 CacheHandler(self.config['cache_location'])
430 )
431
432 elif isinstance(cache, urllib2.OpenerDirector):
433 # If passed something from urllib2.build_opener, use that
434 log().debug("Using %r as urlopener" % cache)
435 self.config['cache_enabled'] = True
436 self.urlopener = cache
437
438 else:
439 raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache)))
440
441 self.config['banners_enabled'] = banners
442 self.config['actors_enabled'] = actors
443
444 if self.config['debug_enabled']:
445 warnings.warn("The debug argument to tvdb_api.__init__ will be removed in the next version. "
446 "To enable debug messages, use the following code before importing: "
447 "import logging; logging.basicConfig(level=logging.DEBUG)")
448 logging.basicConfig(level=logging.DEBUG)
449
450
451 # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml
452 # Hard-coded here as it is realtively static, and saves another HTTP request, as
453 # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
454 self.config['valid_languages'] = [
455 "da", "fi", "nl", "de", "it", "es", "fr","pl", "hu","el","tr",
456 "ru","he","ja","pt","zh","cs","sl", "hr","ko","en","sv","no"
457 ]
458
459 # thetvdb.com should be based around numeric language codes,
460 # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16
461 # requires the language ID, thus this mapping is required (mainly
462 # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations)
463 self.config['langabbv_to_id'] = {'el': 20, 'en': 7, 'zh': 27,
464 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9,
465 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11,
466 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30}
467
468 if language is None:
469 self.config['language'] = 'en'
470 else:
471 if language not in self.config['valid_languages']:
472 raise ValueError("Invalid language %s, options are: %s" % (
473 language, self.config['valid_languages']
474 ))
475 else:
476 self.config['language'] = language
477
478 # The following url_ configs are based of the
479 # http://thetvdb.com/wiki/index.php/Programmers_API
480 self.config['base_url'] = "https://api.thetvdb.com"
481
482 if self.config['search_all_languages']:
483 self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config
484 else:
485 self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % self.config
486
487 self.config['url_epInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml" % self.config
488 self.config['url_epInfo_zip'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip" % self.config
489
490 self.config['url_seriesInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml" % self.config
491 self.config['url_actorsInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config
492
493 self.config['url_seriesBanner'] = u"%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config
494 self.config['url_artworkPrefix'] = u"%(base_url)s/banners/%%s" % self.config
495
496 def _getTempDir(self):
497 """Returns the [system temp dir]/tvdb_api-u501 (or
498 tvdb_api-myuser)
499 """
500 if hasattr(os, 'getuid'):
501 uid = "u%d" % (os.getuid())
502 else:
503 # For Windows
504 try:
505 uid = getpass.getuser()
506 except ImportError:
507 return os.path.join(tempfile.gettempdir(), "tvdb_api")
508
509 return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid))
510
511 def _loadUrl(self, url, recache = False, language=None):
512 global lastTimeout
513 try:
514 log().debug("Retrieving URL %s" % url)
515 resp = self.urlopener.open(url)
516 if 'x-local-cache' in resp.headers:
517 log().debug("URL %s was cached in %s" % (
518 url,
519 resp.headers['x-local-cache'])
520 )
521 if recache:
522 log().debug("Attempting to recache %s" % url)
523 resp.recache()
524 except (IOError, urllib2.URLError), errormsg:
525 if not str(errormsg).startswith('HTTP Error'):
526 lastTimeout = datetime.datetime.now()
527 raise tvdb_error("Could not connect to server: %s" % (errormsg))
528
529
530 # handle gzipped content,
531 # http://dbr.lighthouseapp.com/projects/13342/tickets/72-gzipped-data-patch
532 if 'gzip' in resp.headers.get("Content-Encoding", ''):
533 if gzip:
534 stream = StringIO.StringIO(resp.read())
535 gz = gzip.GzipFile(fileobj=stream)
536 return gz.read()
537
538 raise tvdb_error("Received gzip data from thetvdb.com, but could not correctly handle it")
539
540 if 'application/zip' in resp.headers.get("Content-Type", ''):
541 try:
542 # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20]
543 log().debug("We recived a zip file unpacking now ...")
544 zipdata = StringIO.StringIO()
545 zipdata.write(resp.read())
546 myzipfile = zipfile.ZipFile(zipdata)
547 return myzipfile.read('%s.xml' % language)
548 except zipfile.BadZipfile:
549 if 'x-local-cache' in resp.headers:
550 resp.delete_cache()
551 raise tvdb_error("Bad zip file received from thetvdb.com, could not read it")
552
553 return resp.read()
554
555 def _getetsrc(self, url, language=None):
556 """Loads a URL using caching, returns an ElementTree of the source
557 """
558 src = self._loadUrl(url, language=language)
559 try:
560 # TVDB doesn't sanitize \r (CR) from user input in some fields,
561 # remove it to avoid errors. Change from SickBeard, from will14m
562 return ElementTree.fromstring(src.rstrip("\r"))
563 except SyntaxError:
564 src = self._loadUrl(url, recache=True, language=language)
565 try:
566 return ElementTree.fromstring(src.rstrip("\r"))
567 except SyntaxError, exceptionmsg:
568 errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % (
569 exceptionmsg
570 )
571
572 if self.config['cache_enabled']:
573 errormsg += "\nFirst try emptying the cache folder at..\n%s" % (
574 self.config['cache_location']
575 )
576
577 errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on"
578 errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n"
579 raise tvdb_error(errormsg)
580
581 def _setItem(self, sid, seas, ep, attrib, value):
582 """Creates a new episode, creating Show(), Season() and
583 Episode()s as required. Called by _getShowData to populate show
584
585 Since the nice-to-use tvdb[1][24]['name] interface
586 makes it impossible to do tvdb[1][24]['name] = "name"
587 and still be capable of checking if an episode exists
588 so we can raise tvdb_shownotfound, we have a slightly
589 less pretty method of setting items.. but since the API
590 is supposed to be read-only, this is the best way to
591 do it!
592 The problem is that calling tvdb[1][24]['episodename'] = "name"
593 calls __getitem__ on tvdb[1], there is no way to check if
594 tvdb.__dict__ should have a key "1" before we auto-create it
595 """
596 if sid not in self.shows:
597 self.shows[sid] = Show()
598 if seas not in self.shows[sid]:
599 self.shows[sid][seas] = Season(show = self.shows[sid])
600 if ep not in self.shows[sid][seas]:
601 self.shows[sid][seas][ep] = Episode(season = self.shows[sid][seas])
602 self.shows[sid][seas][ep][attrib] = value
603
604 def _setShowData(self, sid, key, value):
605 """Sets self.shows[sid] to a new Show instance, or sets the data
606 """
607 if sid not in self.shows:
608 self.shows[sid] = Show()
609 self.shows[sid].data[key] = value
610
611 def _cleanData(self, data):
612 """Cleans up strings returned by TheTVDB.com
613
614 Issues corrected:
615 - Replaces & with &
616 - Trailing whitespace
617 """
618 data = data.replace(u"&", u"&")
619 data = data.strip()
620 return data
621
622 def search(self, series):
623 """This searches TheTVDB.com for the series name
624 and returns the result list
625 """
626 series = urllib.quote(series.encode("utf-8"))
627 log().debug("Searching for show %s" % series)
628 seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
629 allSeries = []
630 for series in seriesEt:
631 result = dict((k.tag.lower(), k.text) for k in series.getchildren())
632 result['id'] = int(result['id'])
633 result['lid'] = self.config['langabbv_to_id'][result['language']]
634 if 'aliasnames' in result:
635 result['aliasnames'] = result['aliasnames'].split("|")
636 log().debug('Found series %(seriesname)s' % result)
637 allSeries.append(result)
638
639 return allSeries
640
641 def _getSeries(self, series):
642 """This searches TheTVDB.com for the series name,
643 If a custom_ui UI is configured, it uses this to select the correct
644 series. If not, and interactive == True, ConsoleUI is used, if not
645 BaseUI is used to select the first result.
646 """
647 allSeries = self.search(series)
648
649 if len(allSeries) == 0:
650 log().debug('Series result returned zero')
651 raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
652
653 if self.config['custom_ui'] is not None:
654 log().debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
655 ui = self.config['custom_ui'](config = self.config)
656 else:
657 if not self.config['interactive']:
658 log().debug('Auto-selecting first search result using BaseUI')
659 ui = BaseUI(config = self.config)
660 else:
661 log().debug('Interactively selecting show using ConsoleUI')
662 ui = ConsoleUI(config = self.config)
663
664 return ui.selectSeries(allSeries)
665
666 def _parseBanners(self, sid):
667 """Parses banners XML, from
668 http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
669
670 Banners are retrieved using t['show name]['_banners'], for example:
671
672 >>> t = Tvdb(banners = True)
673 >>> t['scrubs']['_banners'].keys()
674 ['fanart', 'poster', 'series', 'season']
675 >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
676 u'http://thetvdb.com/banners/posters/76156-2.jpg'
677 >>>
678
679 Any key starting with an underscore has been processed (not the raw
680 data from the XML)
681
682 This interface will be improved in future versions.
683 """
684 log().debug('Getting season banners for %s' % (sid))
685 bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
686 banners = {}
687 for cur_banner in bannersEt.findall('Banner'):
688 bid = cur_banner.find('id').text
689 btype = cur_banner.find('BannerType')
690 btype2 = cur_banner.find('BannerType2')
691 if btype is None or btype2 is None:
692 continue
693 btype, btype2 = btype.text, btype2.text
694 if not btype in banners:
695 banners[btype] = {}
696 if not btype2 in banners[btype]:
697 banners[btype][btype2] = {}
698 if not bid in banners[btype][btype2]:
699 banners[btype][btype2][bid] = {}
700
701 for cur_element in cur_banner.getchildren():
702 tag = cur_element.tag.lower()
703 value = cur_element.text
704 if tag is None or value is None:
705 continue
706 tag, value = tag.lower(), value.lower()
707 banners[btype][btype2][bid][tag] = value
708
709 for k, v in banners[btype][btype2][bid].items():
710 if k.endswith("path"):
711 new_key = "_%s" % (k)
712 log().debug("Transforming %s to %s" % (k, new_key))
713 new_url = self.config['url_artworkPrefix'] % (v)
714 banners[btype][btype2][bid][new_key] = new_url
715
716 self._setShowData(sid, "_banners", banners)
717
718 def _parseActors(self, sid):
719 """Parsers actors XML, from
720 http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml
721
722 Actors are retrieved using t['show name]['_actors'], for example:
723
724 >>> t = Tvdb(actors = True)
725 >>> actors = t['scrubs']['_actors']
726 >>> type(actors)
727 <class 'tvdb_api.Actors'>
728 >>> type(actors[0])
729 <class 'tvdb_api.Actor'>
730 >>> actors[0]
731 <Actor "Zach Braff">
732 >>> sorted(actors[0].keys())
733 ['id', 'image', 'name', 'role', 'sortorder']
734 >>> actors[0]['name']
735 u'Zach Braff'
736 >>> actors[0]['image']
737 u'http://thetvdb.com/banners/actors/43640.jpg'
738
739 Any key starting with an underscore has been processed (not the raw
740 data from the XML)
741 """
742 log().debug("Getting actors for %s" % (sid))
743 actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
744
745 cur_actors = Actors()
746 for curActorItem in actorsEt.findall("Actor"):
747 curActor = Actor()
748 for curInfo in curActorItem:
749 tag = curInfo.tag.lower()
750 value = curInfo.text
751 if value is not None:
752 if tag == "image":
753 value = self.config['url_artworkPrefix'] % (value)
754 else:
755 value = self._cleanData(value)
756 curActor[tag] = value
757 cur_actors.append(curActor)
758 self._setShowData(sid, '_actors', cur_actors)
759
760 def _getShowData(self, sid, language):
761 """Takes a series ID, gets the epInfo URL and parses the TVDB
762 XML file into the shows dict in layout:
763 shows[series_id][season_number][episode_number]
764 """
765
766 if self.config['language'] is None:
767 log().debug('Config language is none, using show language')
768 if language is None:
769 raise tvdb_error("config['language'] was None, this should not happen")
770 getShowInLanguage = language
771 else:
772 log().debug(
773 'Configured language %s override show language of %s' % (
774 self.config['language'],
775 language
776 )
777 )
778 getShowInLanguage = self.config['language']
779
780 # Parse show information
781 log().debug('Getting all series data for %s' % (sid))
782 seriesInfoEt = self._getetsrc(
783 self.config['url_seriesInfo'] % (sid, getShowInLanguage)
784 )
785 for curInfo in seriesInfoEt.findall("Series")[0]:
786 tag = curInfo.tag.lower()
787 value = curInfo.text
788
789 if value is not None:
790 if tag in ['banner', 'fanart', 'poster']:
791 value = self.config['url_artworkPrefix'] % (value)
792 else:
793 value = self._cleanData(value)
794
795 self._setShowData(sid, tag, value)
796
797 # Parse banners
798 if self.config['banners_enabled']:
799 self._parseBanners(sid)
800
801 # Parse actors
802 if self.config['actors_enabled']:
803 self._parseActors(sid)
804
805 # Parse episode data
806 log().debug('Getting all episodes of %s' % (sid))
807
808 if self.config['useZip']:
809 url = self.config['url_epInfo_zip'] % (sid, language)
810 else:
811 url = self.config['url_epInfo'] % (sid, language)
812
813 epsEt = self._getetsrc( url, language=language)
814
815 for cur_ep in epsEt.findall("Episode"):
816
817 if self.config['dvdorder']:
818 log().debug('Using DVD ordering.')
819 use_dvd = cur_ep.find('DVD_season').text != None and cur_ep.find('DVD_episodenumber').text != None
820 else:
821 use_dvd = False
822
823 if use_dvd:
824 seas_no = int(cur_ep.find('DVD_season').text)
825 ep_no = int(float(cur_ep.find('DVD_episodenumber').text))
826 else:
827 seas_no = int(cur_ep.find('SeasonNumber').text)
828 ep_no = int(cur_ep.find('EpisodeNumber').text)
829
830 for cur_item in cur_ep.getchildren():
831 tag = cur_item.tag.lower()
832 value = cur_item.text
833 if value is not None:
834 if tag == 'filename':
835 value = self.config['url_artworkPrefix'] % (value)
836 else:
837 value = self._cleanData(value)
838 self._setItem(sid, seas_no, ep_no, tag, value)
839
840 def _nameToSid(self, name):
841 """Takes show name, returns the correct series ID (if the show has
842 already been grabbed), or grabs all episodes and returns
843 the correct SID.
844 """
845 if name in self.corrections:
846 log().debug('Correcting %s to %s' % (name, self.corrections[name]) )
847 sid = self.corrections[name]
848 else:
849 log().debug('Getting show %s' % (name))
850 selected_series = self._getSeries( name )
851 sname, sid = selected_series['seriesname'], selected_series['id']
852 log().debug('Got %(seriesname)s, id %(id)s' % selected_series)
853
854 self.corrections[name] = sid
855 self._getShowData(selected_series['id'], selected_series['language'])
856
857 return sid
858
859 def __getitem__(self, key):
860 """Handles tvdb_instance['seriesname'] calls.
861 The dict index should be the show id
862 """
863 if isinstance(key, (int, long)):
864 # Item is integer, treat as show id
865 if key not in self.shows:
866 self._getShowData(key, self.config['language'])
867 return self.shows[key]
868
869 key = key.lower() # make key lower case
870 sid = self._nameToSid(key)
871 log().debug('Got series id %s' % (sid))
872 return self.shows[sid]
873
874 def __repr__(self):
875 return str(self.shows)
876
877
878def main():
879 """Simple example of using tvdb_api - it just
880 grabs an episode name interactively.
881 """
882 import logging
883 logging.basicConfig(level=logging.DEBUG)
884
885 tvdb_instance = Tvdb(interactive=True, cache=False)
886 print tvdb_instance['Lost']['seriesname']
887 print tvdb_instance['Lost'][1][4]['episodename']
888
889if __name__ == '__main__':
890 main()