Advertisement
Guest User

Untitled

a guest
Nov 27th, 2019
242
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 32.96 KB | None | 0 0
  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.  
  10. Example usage:
  11.  
  12. >>> from tvdb_api import Tvdb
  13. >>> t = Tvdb()
  14. >>> t['Lost'][4][11]['episodename']
  15. u'Cabin Fever'
  16. """
  17. __author__ = "dbr/Ben"
  18. __version__ = "1.9"
  19.  
  20. import os
  21. import time
  22. import urllib
  23. import urllib2
  24. import getpass
  25. import StringIO
  26. import tempfile
  27. import warnings
  28. import logging
  29. import datetime
  30. import zipfile
  31.  
  32. try:
  33. import xml.etree.cElementTree as ElementTree
  34. except ImportError:
  35. import xml.etree.ElementTree as ElementTree
  36.  
  37. try:
  38. import gzip
  39. except ImportError:
  40. gzip = None
  41.  
  42.  
  43. from tvdb_cache import CacheHandler
  44.  
  45. from tvdb_ui import BaseUI, ConsoleUI
  46. from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
  47. tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
  48.  
  49. lastTimeout = None
  50.  
  51. def log():
  52. return logging.getLogger("tvdb_api")
  53.  
  54.  
  55. class 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.  
  81. class 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.  
  178. class 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.  
  216. class 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.  
  272. class Actors(list):
  273. """Holds all Actor instances for a show
  274. """
  275. pass
  276.  
  277.  
  278. class 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.  
  291. class 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 &amp; with &
  616. - Trailing whitespace
  617. """
  618. data = data.replace(u"&amp;", 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.  
  878. def 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.  
  889. if __name__ == '__main__':
  890. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement