diff -Nru tvdb-api-1.10/debian/changelog tvdb-api-2.0/debian/changelog --- tvdb-api-1.10/debian/changelog 2015-06-06 01:10:02.000000000 +0000 +++ tvdb-api-2.0/debian/changelog 2018-06-11 01:44:23.000000000 +0000 @@ -1,3 +1,20 @@ +tvdb-api (2.0-1) unstable; urgency=medium + + [ Ondřej Nový ] + * Fixed VCS URL (https) + * d/control: Set Vcs-* to salsa.debian.org + * d/copyright: Use https protocol in Format field + * d/watch: Use https protocol + + [ Sandro Tosi ] + * New upstream release + * debian/copyright + - extend packaging copyright years + * debian/control + - bump Standards-Version to 4.1.4 (no changes needed) + + -- Sandro Tosi Sun, 10 Jun 2018 21:44:23 -0400 + tvdb-api (1.10-1) unstable; urgency=medium * New upstream release diff -Nru tvdb-api-1.10/debian/control tvdb-api-2.0/debian/control --- tvdb-api-1.10/debian/control 2015-04-11 17:36:46.000000000 +0000 +++ tvdb-api-2.0/debian/control 2018-06-11 01:44:23.000000000 +0000 @@ -5,10 +5,10 @@ Uploaders: Debian Python Modules Team Build-Depends: debhelper (>= 9), python-all, dh-python, python-setuptools, python3, python3-setuptools XS-Python-Version: all -Standards-Version: 3.9.6 +Standards-Version: 4.1.4 Homepage: https://github.com/dbr/tvdb_api -Vcs-Svn: svn://anonscm.debian.org/python-modules/packages/tvdb-api/trunk/ -Vcs-Browser: http://anonscm.debian.org/viewvc/python-modules/packages/tvdb-api/trunk/ +Vcs-Git: https://salsa.debian.org/python-team/modules/tvdb-api.git +Vcs-Browser: https://salsa.debian.org/python-team/modules/tvdb-api Package: python-tvdb-api Architecture: all diff -Nru tvdb-api-1.10/debian/copyright tvdb-api-2.0/debian/copyright --- tvdb-api-1.10/debian/copyright 2015-04-11 17:06:15.000000000 +0000 +++ tvdb-api-2.0/debian/copyright 2018-06-11 01:44:23.000000000 +0000 @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: tvdb-api Source: https://github.com/dbr/tvdb_api @@ -31,7 +31,7 @@ For more information, please refer to Files: debian/* -Copyright: 2012-2015 Sandro Tosi +Copyright: 2012-2018 Sandro Tosi License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff -Nru tvdb-api-1.10/debian/.git-dpm tvdb-api-2.0/debian/.git-dpm --- tvdb-api-1.10/debian/.git-dpm 1970-01-01 00:00:00.000000000 +0000 +++ tvdb-api-2.0/debian/.git-dpm 2018-06-11 01:44:23.000000000 +0000 @@ -0,0 +1,11 @@ +# see git-dpm(1) from git-dpm package +4b1089dac1e5136587460f1d0cb1834b4ae061d3 +4b1089dac1e5136587460f1d0cb1834b4ae061d3 +4b1089dac1e5136587460f1d0cb1834b4ae061d3 +4b1089dac1e5136587460f1d0cb1834b4ae061d3 +tvdb-api_1.10.orig.tar.gz +7104ad73e760b653c347c95d84625f8d22548950 +34630 +debianTag="debian/%e%v" +patchedTag="patched/%e%v" +upstreamTag="upstream/%e%u" diff -Nru tvdb-api-1.10/debian/watch tvdb-api-2.0/debian/watch --- tvdb-api-1.10/debian/watch 2015-04-11 17:04:53.000000000 +0000 +++ tvdb-api-2.0/debian/watch 2018-06-11 01:44:23.000000000 +0000 @@ -1,3 +1,3 @@ version=3 opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ -http://pypi.debian.net/tvdb_api/tvdb_api-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) \ No newline at end of file +https://pypi.debian.net/tvdb_api/tvdb_api-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) \ No newline at end of file diff -Nru tvdb-api-1.10/PKG-INFO tvdb-api-2.0/PKG-INFO --- tvdb-api-1.10/PKG-INFO 2014-11-08 13:41:03.000000000 +0000 +++ tvdb-api-2.0/PKG-INFO 2017-09-16 10:32:01.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: tvdb_api -Version: 1.10 +Version: 2.0 Summary: Interface to thetvdb.com Home-page: http://github.com/dbr/tvdb_api/tree/master Author: dbr/Ben @@ -14,7 +14,7 @@ >>> ep = t['My Name Is Earl'][1][22] >>> ep - >>> ep['episodename'] + >>> ep['episodeName'] u'Stole a Badge' Platform: UNKNOWN @@ -27,6 +27,8 @@ Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Multimedia Classifier: Topic :: Utilities Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -Nru tvdb-api-1.10/Rakefile tvdb-api-2.0/Rakefile --- tvdb-api-1.10/Rakefile 2014-11-08 13:40:58.000000000 +0000 +++ tvdb-api-2.0/Rakefile 2017-08-30 05:32:25.000000000 +0000 @@ -47,7 +47,7 @@ end desc "Upload current version to PyPi" -task :topypi do +task :topypi => :test do cur_file = File.open("tvdb_api.py").read() tvdb_api_version = cur_file.scan(/__version__ = "(.*)"/) tvdb_api_version = tvdb_api_version[0][0] diff -Nru tvdb-api-1.10/readme.md tvdb-api-2.0/readme.md --- tvdb-api-1.10/readme.md 2014-10-26 10:58:15.000000000 +0000 +++ tvdb-api-2.0/readme.md 2017-08-30 05:32:25.000000000 +0000 @@ -65,7 +65,7 @@ The data is stored in an attribute named `data`, within the Show instance: >>> t['scrubs'].data.keys() - ['networkid', 'rating', 'airs_dayofweek', 'contentrating', 'seriesname', 'id', 'airs_time', 'network', 'fanart', 'lastupdated', 'actors', 'ratingcount', 'status', 'added', 'poster', 'imdb_id', 'genre', 'banner', 'seriesid', 'language', 'zap2it_id', 'addedby', 'tms_wanted', 'firstaired', 'runtime', 'overview'] + ['networkid', 'rating', 'airs_dayofweek', 'contentrating', 'seriesname', 'id', 'airs_time', 'network', 'fanart', 'lastupdated', 'actors', 'ratingcount', 'status', 'added', 'poster', 'tms_wanted_old', 'imdb_id', 'genre', 'banner', 'seriesid', 'language', 'zap2it_id', 'addedby', 'firstaired', 'runtime', 'overview'] Although each element is also accessible via `t['scrubs']` for ease-of-use: @@ -105,7 +105,7 @@ Remember a simple list of actors is accessible via the default Show data: >>> t['scrubs']['actors'] - u'|Zach Braff|Donald Faison|Sarah Chalke|Christa Miller|Aloma Wright|Robert Maschio|Sam Lloyd|Neil Flynn|Ken Jenkins|Judy Reyes|John C. McGinley|Travis Schuldt|Johnny Kastl|Heather Graham|Michael Mosley|Kerry Bish\xe9|Dave Franco|Eliza Coupe|' + u'|Zach Braff|Donald Faison|Sarah Chalke|Judy Reyes|John C. McGinley|Neil Flynn|Ken Jenkins|Christa Miller|Aloma Wright|Robert Maschio|Sam Lloyd|Travis Schuldt|Johnny Kastl|Heather Graham|Michael Mosley|Kerry Bish\xe9|Dave Franco|Eliza Coupe|' [tvdb]: http://thetvdb.com [tvnamer]: http://github.com/dbr/tvnamer diff -Nru tvdb-api-1.10/setup.py tvdb-api-2.0/setup.py --- tvdb-api-1.10/setup.py 2014-11-08 13:38:31.000000000 +0000 +++ tvdb-api-2.0/setup.py 2017-09-16 10:24:32.000000000 +0000 @@ -1,25 +1,34 @@ import sys from setuptools import setup +from setuptools.command.test import test as TestCommand -IS_PY2 = sys.version_info[0] == 2 -_requirements = [] -if not IS_PY2: - _requirements.append('requests_cache') - - # 'requests' is installed as requirement by requests-cache, - # commented out because it triggers a bug in setuptool: - # https://bitbucket.org/pypa/setuptools/issue/196/tests_require-pytest-pytest-cov-breaks +class PyTest(TestCommand): + user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] + def initialize_options(self): + TestCommand.initialize_options(self) + self.pytest_args = [] + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + #import here, cause outside the eggs aren't loaded + import pytest + errno = pytest.main(self.pytest_args) + sys.exit(errno) + + +_requirements = ['requests_cache', 'requests'] _modules = ['tvdb_api', 'tvdb_ui', 'tvdb_exceptions'] -if IS_PY2: - _modules.append('tvdb_cache') setup( name = 'tvdb_api', -version='1.10', +version='2.0', author='dbr/Ben', description='Interface to thetvdb.com', @@ -35,13 +44,16 @@ >>> ep = t['My Name Is Earl'][1][22] >>> ep ->>> ep['episodename'] +>>> ep['episodeName'] u'Stole a Badge' """, py_modules = _modules, install_requires = _requirements, +tests_require=['pytest'], +cmdclass = {'test': PyTest}, + classifiers=[ "Intended Audience :: Developers", "Natural Language :: English", @@ -52,6 +64,8 @@ "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Topic :: Multimedia", "Topic :: Utilities", "Topic :: Software Development :: Libraries :: Python Modules", diff -Nru tvdb-api-1.10/tests/test_tvdb_api.py tvdb-api-2.0/tests/test_tvdb_api.py --- tvdb-api-1.10/tests/test_tvdb_api.py 2014-10-27 12:16:49.000000000 +0000 +++ tvdb-api-2.0/tests/test_tvdb_api.py 2017-09-16 10:12:16.000000000 +0000 @@ -11,75 +11,58 @@ import os import sys import datetime -import unittest +import pytest # Force parent directory onto path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import tvdb_api -import tvdb_ui from tvdb_api import (tvdb_shownotfound, tvdb_seasonnotfound, -tvdb_episodenotfound, tvdb_attributenotfound) + tvdb_episodenotfound, tvdb_attributenotfound) -IS_PY2 = sys.version_info[0] == 2 - - -class test_tvdb_basic(unittest.TestCase): +class TestTvdbBasic: # Used to store the cached instance of Tvdb() t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, banners=False) + def test_different_case(self): """Checks the auto-correction of show names is working. It should correct the weirdly capitalised 'sCruBs' to 'Scrubs' """ - self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady') - self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs') + assert self.t['scrubs'][1][4]['episodeName'] == 'My Old Lady' + assert self.t['sCruBs']['seriesName'] == 'Scrubs' def test_spaces(self): """Checks shownames with spaces """ - self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl') - self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death') + assert self.t['My Name Is Earl']['seriesName'] == 'My Name Is Earl' + assert self.t['My Name Is Earl'][1][4]['episodeName'] == 'Faked My Own Death' def test_numeric(self): """Checks numeric show names """ - self.assertEquals(self.t['24'][2][20]['episodename'], 'Day 2: 3:00 A.M.-4:00 A.M.') - self.assertEquals(self.t['24']['seriesname'], '24') + assert self.t['24'][2][20]['episodeName'] == 'Day 2: 3:00 A.M. - 4:00 A.M.' + assert self.t['24']['seriesName'] == '24' def test_show_iter(self): """Iterating over a show returns each seasons """ - self.assertEquals( - len( - [season for season in self.t['Life on Mars']] - ), - 2 - ) - + assert len([season for season in self.t['Life on Mars']]) == 2 + def test_season_iter(self): """Iterating over a show returns episodes """ - self.assertEquals( - len( - [episode for episode in self.t['Life on Mars'][1]] - ), - 8 - ) + assert len([episode for episode in self.t['Life on Mars'][1]]) == 8 def test_get_episode_overview(self): """Checks episode overview is retrieved correctly. """ - self.assertEquals( - self.t['Battlestar Galactica (2003)'][1][6]['overview'].startswith( - 'When a new copy of Doral, a Cylon who had been previously'), - True - ) + assert self.t['Battlestar Galactica (2003)'][1][6]['overview'].startswith('When a new copy of Doral, a Cylon who had been previously') == True def test_get_parent(self): """Check accessing series from episode instance @@ -88,329 +71,251 @@ season = show[1] episode = show[1][1] - self.assertEquals( - season.show, - show - ) - - self.assertEquals( - episode.season, - season - ) - - self.assertEquals( - episode.season.show, - show - ) + assert season.show == show + assert episode.season == season + assert episode.season.show == show def test_no_season(self): show = self.t['Katekyo Hitman Reborn'] print(tvdb_api) print(show[1][1]) -class test_tvdb_errors(unittest.TestCase): - # Used to store the cached instance of Tvdb() + +class TestTvdbErrors: t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_seasonnotfound(self): """Checks exception is thrown when season doesn't exist. """ - self.assertRaises(tvdb_seasonnotfound, lambda:self.t['CNNNN'][10][1]) + with pytest.raises(tvdb_seasonnotfound): + self.t['CNNNN'][10] def test_shownotfound(self): """Checks exception is thrown when episode doesn't exist. """ - self.assertRaises(tvdb_shownotfound, lambda:self.t['the fake show thingy']) - + with pytest.raises(tvdb_shownotfound): + self.t['the fake show thingy'] + def test_episodenotfound(self): """Checks exception is raised for non-existent episode """ - self.assertRaises(tvdb_episodenotfound, lambda:self.t['Scrubs'][1][30]) + with pytest.raises(tvdb_episodenotfound): + self.t['Scrubs'][1][30] def test_attributenamenotfound(self): """Checks exception is thrown for if an attribute isn't found. """ - self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN'][1][6]['afakeattributething']) - self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN']['afakeattributething']) + with pytest.raises(tvdb_attributenotfound): + self.t['CNNNN'][1][6]['afakeattributething'] + self.t['CNNNN']['afakeattributething'] + -class test_tvdb_search(unittest.TestCase): +class TestTvdbSearch: # Used to store the cached instance of Tvdb() t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_search_len(self): """There should be only one result matching """ - self.assertEquals(len(self.t['My Name Is Earl'].search('Faked His Own Death')), 1) + assert len(self.t['My Name Is Earl'].search('Faked My Own Death')) == 1 def test_search_checkname(self): """Checks you can get the episode name of a search result """ - self.assertEquals(self.t['Scrubs'].search('my first')[0]['episodename'], 'My First Day') - self.assertEquals(self.t['My Name Is Earl'].search('Faked His Own Death')[0]['episodename'], 'Faked His Own Death') - + assert self.t['Scrubs'].search('my first')[0]['episodeName'] == 'My First Day' + assert self.t['My Name Is Earl'].search('Faked My Own Death')[0]['episodeName'] == 'Faked My Own Death' + def test_search_multiresults(self): """Checks search can return multiple results """ - self.assertEquals(len(self.t['Scrubs'].search('my first')) >= 3, True) + assert (len(self.t['Scrubs'].search('my first')) >= 3) == True def test_search_no_params_error(self): """Checks not supplying search info raises TypeError""" - self.assertRaises( - TypeError, - lambda: self.t['Scrubs'].search() - ) + with pytest.raises(TypeError): + self.t['Scrubs'].search() def test_search_season(self): """Checks the searching of a single season""" - self.assertEquals( - len(self.t['Scrubs'][1].search("First")), - 3 - ) - + assert len(self.t['Scrubs'][1].search("First")) == 3 + def test_search_show(self): """Checks the searching of an entire show""" - self.assertEquals( - len(self.t['CNNNN'].search('CNNNN', key='episodename')), - 3 - ) + assert len(self.t['CNNNN'].search('CNNNN', key='episodeName')) == 3 def test_aired_on(self): """Tests airedOn show method""" sr = self.t['Scrubs'].airedOn(datetime.date(2001, 10, 2)) - self.assertEquals(len(sr), 1) - self.assertEquals(sr[0]['episodename'], u'My First Day') + assert len(sr) == 1 + assert sr[0]['episodeName'] == u'My First Day' + -class test_tvdb_data(unittest.TestCase): +class TestTvdbData: # Used to store the cached instance of Tvdb() t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_episode_data(self): """Check the firstaired value is retrieved """ - self.assertEquals( - self.t['lost']['firstaired'], - '2004-09-22' - ) + assert self.t['lost']['firstAired'] == '2004-09-22' + -class test_tvdb_misc(unittest.TestCase): +class TestTvdbMisc: # Used to store the cached instance of Tvdb() t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_repr_show(self): """Check repr() of Season """ - self.assertEquals( - repr(self.t['CNNNN']), - "" - ) + assert repr(self.t['CNNNN']).replace("u'", "'") == "" + def test_repr_season(self): """Check repr() of Season """ - self.assertEquals( - repr(self.t['CNNNN'][1]), - "" - ) + assert repr(self.t['CNNNN'][1]) == "" + def test_repr_episode(self): """Check repr() of Episode """ - self.assertEquals( - repr(self.t['CNNNN'][1][1]), - "" - ) + assert repr(self.t['CNNNN'][1][1]).replace("u'", "'") == "" + def test_have_all_languages(self): """Check valid_languages is up-to-date (compared to languages.xml) """ - et = self.t._getetsrc( - "http://thetvdb.com/api/%s/languages.xml" % ( - self.t.config['apikey'] - ) - ) - languages = [x.find("abbreviation").text for x in et.findall("Language")] - - self.assertEquals( - sorted(languages), - sorted(self.t.config['valid_languages']) - ) - -class test_tvdb_languages(unittest.TestCase): + et = self.t._getetsrc("https://api.thetvdb.com/languages") + + languages = [x['abbreviation'] for x in et] + + assert sorted(languages) == sorted(self.t.config['valid_languages']) + + +class TestTvdbLanguages: def test_episode_name_french(self): """Check episode data is in French (language="fr") """ t = tvdb_api.Tvdb(cache = True, language = "fr") - self.assertEquals( - t['scrubs'][1][1]['episodename'], - "Mon premier jour" - ) - self.assertTrue( - t['scrubs']['overview'].startswith( - u"J.D. est un jeune m\xe9decin qui d\xe9bute" - ) - ) + assert t['scrubs'][1][1]['episodeName'] == "Mon premier jour" + assert t['scrubs']['overview'].startswith(u"J.D. est un jeune m\xe9decin qui d\xe9bute") def test_episode_name_spanish(self): """Check episode data is in Spanish (language="es") """ t = tvdb_api.Tvdb(cache = True, language = "es") - self.assertEquals( - t['scrubs'][1][1]['episodename'], - "Mi Primer Dia" - ) - self.assertTrue( - t['scrubs']['overview'].startswith( - u'Scrubs es una divertida comedia' - ) - ) + assert t['scrubs'][1][1]['episodeName'] == u'Mi primer día' + assert t['scrubs']['overview'].startswith(u'Scrubs es una divertida comedia') def test_multilanguage_selection(self): """Check selected language is used """ - class SelectEnglishUI(tvdb_ui.BaseUI): - def selectSeries(self, allSeries): - return [x for x in allSeries if x['language'] == "en"][0] - - class SelectItalianUI(tvdb_ui.BaseUI): - def selectSeries(self, allSeries): - return [x for x in allSeries if x['language'] == "it"][0] - t_en = tvdb_api.Tvdb( cache=True, - custom_ui = SelectEnglishUI, language = "en") t_it = tvdb_api.Tvdb( cache=True, - custom_ui = SelectItalianUI, language = "it") - self.assertEquals( - t_en['dexter'][1][2]['episodename'], "Crocodile" - ) - self.assertEquals( - t_it['dexter'][1][2]['episodename'], "Lacrime di coccodrillo" - ) + assert t_en['dexter'][1][2]['episodeName'] == "Crocodile" + assert t_it['dexter'][1][2]['episodeName'] == "Lacrime di coccodrillo" -class test_tvdb_unicode(unittest.TestCase): +class TestTvdbUnicode: def test_search_in_chinese(self): """Check searching for show with language=zh returns Chinese seriesname """ - t = tvdb_api.Tvdb(cache = True, language = "zh") + t = tvdb_api.Tvdb(cache=True, language="zh") show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'] - self.assertEquals( - type(show), - tvdb_api.Show - ) - - self.assertEquals( - show['seriesname'], - u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i' - ) + assert type(show) == tvdb_api.Show + assert show['seriesName'] == u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i' + @pytest.mark.skip('Новое API не возвращает сразу все языки') def test_search_in_all_languages(self): """Check search_all_languages returns Chinese show, with language=en """ - t = tvdb_api.Tvdb(cache = True, search_all_languages = True, language="en") + t = tvdb_api.Tvdb(cache=True, search_all_languages=True, language="en") show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'] - self.assertEquals( - type(show), - tvdb_api.Show - ) - - self.assertEquals( - show['seriesname'], - u'Virtues Of Harmony II' - ) + assert type(show) == tvdb_api.Show + assert show['seriesName'] == u'Virtues Of Harmony II' + -class test_tvdb_banners(unittest.TestCase): +class TestTvdbBanners: # Used to store the cached instance of Tvdb() t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = True) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, banners=True) def test_have_banners(self): """Check banners at least one banner is found """ - self.assertEquals( - len(self.t['scrubs']['_banners']) > 0, - True - ) + assert (len(self.t['scrubs']['_banners']) > 0) == True def test_banner_url(self): """Checks banner URLs start with http:// """ for banner_type, banner_data in self.t['scrubs']['_banners'].items(): for res, res_data in banner_data.items(): - for bid, banner_info in res_data.items(): - self.assertEquals( - banner_info['_bannerpath'].startswith("http://"), - True - ) + if res != 'raw': + for bid, banner_info in res_data.items(): + assert banner_info['_bannerpath'].startswith("http://") == True + @pytest.mark.skip('В новом API нет картинки у эпизода') def test_episode_image(self): """Checks episode 'filename' image is fully qualified URL """ - self.assertEquals( - self.t['scrubs'][1][1]['filename'].startswith("http://"), - True - ) - + assert self.t['scrubs'][1][1]['filename'].startswith("http://") == True + + @pytest.mark.skip('В новом API у сериала кроме банера больше нет картинок') def test_show_artwork(self): """Checks various image URLs within season data are fully qualified """ for key in ['banner', 'fanart', 'poster']: - self.assertEquals( - self.t['scrubs'][key].startswith("http://"), - True - ) + assert self.t['scrubs'][key].startswith("http://") == True -class test_tvdb_actors(unittest.TestCase): + +class TestTvdbActors: t = None - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, actors=True) def test_actors_is_correct_datatype(self): """Check show/_actors key exists and is correct type""" - self.assertTrue( - isinstance( - self.t['scrubs']['_actors'], - tvdb_api.Actors - ) - ) - + assert isinstance(self.t['scrubs']['_actors'], tvdb_api.Actors) == True + def test_actors_has_actor(self): """Check show has at least one Actor """ - self.assertTrue( - isinstance( - self.t['scrubs']['_actors'][0], - tvdb_api.Actor - ) - ) - + assert isinstance(self.t['scrubs']['_actors'][0], tvdb_api.Actor) == True + def test_actor_has_name(self): """Check first actor has a name""" - self.assertEquals( - self.t['scrubs']['_actors'][0]['name'], - "Zach Braff" - ) + names = [actor['name'] for actor in self.t['scrubs']['_actors']] + + assert u"Zach Braff" in names def test_actor_image_corrected(self): """Check image URL is fully qualified @@ -419,71 +324,38 @@ if actor['image'] is not None: # Actor's image can be None, it displays as the placeholder # image on thetvdb.com - self.assertTrue( - actor['image'].startswith("http://") - ) + assert actor['image'].startswith("http://") == True -class test_tvdb_doctest(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - + +class TestTvdbDoctest: def test_doctest(self): """Check docstring examples works""" import doctest doctest.testmod(tvdb_api) -class test_tvdb_custom_caching(unittest.TestCase): +class TestTvdbCustomCaching: def test_true_false_string(self): """Tests setting cache to True/False/string Basic tests, only checking for errors """ - - tvdb_api.Tvdb(cache = True) - tvdb_api.Tvdb(cache = False) - tvdb_api.Tvdb(cache = "/tmp") + tvdb_api.Tvdb(cache=True) + tvdb_api.Tvdb(cache=False) + tvdb_api.Tvdb(cache="/tmp") def test_invalid_cache_option(self): """Tests setting cache to invalid value """ try: - tvdb_api.Tvdb(cache = 2.3) + tvdb_api.Tvdb(cache=2.3) except ValueError: pass else: - self.fail("Expected ValueError from setting cache to float") - - def test_custom_urlopener(self): - if not IS_PY2: - raise unittest.SkipTest("cannot supply custom opener in Python 3 because requests is used") - - class UsedCustomOpener(Exception): - pass - - import urllib2 - class TestOpener(urllib2.BaseHandler): - def default_open(self, request): - print(request.get_method()) - raise UsedCustomOpener("Something") - - custom_opener = urllib2.build_opener(TestOpener()) - t = tvdb_api.Tvdb(cache = custom_opener) - try: - t['scrubs'] - except UsedCustomOpener: - pass - else: - self.fail("Did not use custom opener") + pytest.fail("Expected ValueError from setting cache to float") def test_custom_request_session(self): - if IS_PY2: - return from requests import Session as OriginalSession class Used(Exception): pass @@ -492,103 +364,82 @@ def request(self, *args, **kwargs): raise Used("Hurray") c = CustomCacheForTest() - t = tvdb_api.Tvdb(cache = c) + t = tvdb_api.Tvdb(cache=c) try: t['scrubs'] except Used: pass else: - self.fail("Did not use custom session") + pytest.fail("Did not use custom session") -class test_tvdb_by_id(unittest.TestCase): +class TestTvdbById: t = None - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, actors=True) def test_actors_is_correct_datatype(self): """Check show/_actors key exists and is correct type""" - self.assertEquals( - self.t[76156]['seriesname'], - 'Scrubs' - ) + assert self.t[76156]['seriesName'] == 'Scrubs' -class test_tvdb_zip(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True) - - def test_get_series_from_zip(self): - """ - """ - self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady') - self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs') - - def test_spaces_from_zip(self): - """Checks shownames with spaces - """ - self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl') - self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death') - - -class test_tvdb_show_ordering(unittest.TestCase): +class TestTvdbShowOrdering: # Used to store the cached instance of Tvdb() t_dvd = None t_air = None - def setUp(self): - if self.t_dvd is None: - self.t_dvd = tvdb_api.Tvdb(cache = True, useZip = True, dvdorder=True) + @classmethod + def setup_class(cls): + if cls.t_dvd is None: + cls.t_dvd = tvdb_api.Tvdb(cache=True, dvdorder=True) - if self.t_air is None: - self.t_air = tvdb_api.Tvdb(cache = True, useZip = True) + if cls.t_air is None: + cls.t_air = tvdb_api.Tvdb(cache=True) def test_ordering(self): """Test Tvdb.search method """ - self.assertEquals(u'The Train Job', self.t_air['Firefly'][1][1]['episodename']) - self.assertEquals(u'Serenity', self.t_dvd['Firefly'][1][1]['episodename']) + assert u'The Train Job' == self.t_air['Firefly'][1][1]['episodeName'] + assert u'Serenity' == self.t_dvd['Firefly'][1][1]['episodeName'] - self.assertEquals(u'The Cat & the Claw (Part 1)', self.t_air['Batman The Animated Series'][1][1]['episodename']) - self.assertEquals(u'On Leather Wings', self.t_dvd['Batman The Animated Series'][1][1]['episodename']) + assert u'The Cat & the Claw (Part 1)' == self.t_air['Batman The Animated Series'][1][1]['episodeName'] + assert u'On Leather Wings' == self.t_dvd['Batman The Animated Series'][1][1]['episodeName'] -class test_tvdb_show_search(unittest.TestCase): + +class TestTvdbShowSearch: # Used to store the cached instance of Tvdb() t = None - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True) + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True) def test_search(self): """Test Tvdb.search method """ results = self.t.search("my name is earl") - all_ids = [x['seriesid'] for x in results] - self.assertTrue('75397' in all_ids) + all_ids = [x['id'] for x in results] + assert 75397 in all_ids -class test_tvdb_alt_names(unittest.TestCase): +class TestTvdbAltNames: t = None - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True) + + @classmethod + def setup_class(cls): + if cls.t is None: + cls.t = tvdb_api.Tvdb(cache=True, actors=True) def test_1(self): """Tests basic access of series name alias """ results = self.t.search("Don't Trust the B---- in Apartment 23") series = results[0] - self.assertTrue( - 'Apartment 23' in series['aliasnames'] - ) - + assert 'Apartment 23' in series['aliases'] if __name__ == '__main__': - runner = unittest.TextTestRunner(verbosity = 2) - unittest.main(testRunner = runner) + pytest.main() diff -Nru tvdb-api-1.10/tvdb_api.egg-info/PKG-INFO tvdb-api-2.0/tvdb_api.egg-info/PKG-INFO --- tvdb-api-1.10/tvdb_api.egg-info/PKG-INFO 2014-11-08 13:41:03.000000000 +0000 +++ tvdb-api-2.0/tvdb_api.egg-info/PKG-INFO 2017-09-16 10:32:01.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: tvdb-api -Version: 1.10 +Version: 2.0 Summary: Interface to thetvdb.com Home-page: http://github.com/dbr/tvdb_api/tree/master Author: dbr/Ben @@ -14,7 +14,7 @@ >>> ep = t['My Name Is Earl'][1][22] >>> ep - >>> ep['episodename'] + >>> ep['episodeName'] u'Stole a Badge' Platform: UNKNOWN @@ -27,6 +27,8 @@ Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Multimedia Classifier: Topic :: Utilities Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -Nru tvdb-api-1.10/tvdb_api.egg-info/requires.txt tvdb-api-2.0/tvdb_api.egg-info/requires.txt --- tvdb-api-1.10/tvdb_api.egg-info/requires.txt 1970-01-01 00:00:00.000000000 +0000 +++ tvdb-api-2.0/tvdb_api.egg-info/requires.txt 2017-09-16 10:32:01.000000000 +0000 @@ -0,0 +1,2 @@ +requests_cache +requests diff -Nru tvdb-api-1.10/tvdb_api.egg-info/SOURCES.txt tvdb-api-2.0/tvdb_api.egg-info/SOURCES.txt --- tvdb-api-1.10/tvdb_api.egg-info/SOURCES.txt 2014-11-08 13:41:03.000000000 +0000 +++ tvdb-api-2.0/tvdb_api.egg-info/SOURCES.txt 2017-09-16 10:32:01.000000000 +0000 @@ -4,7 +4,6 @@ readme.md setup.py tvdb_api.py -tvdb_cache.py tvdb_exceptions.py tvdb_ui.py tests/gprof2dot.py @@ -13,4 +12,5 @@ tvdb_api.egg-info/PKG-INFO tvdb_api.egg-info/SOURCES.txt tvdb_api.egg-info/dependency_links.txt +tvdb_api.egg-info/requires.txt tvdb_api.egg-info/top_level.txt \ No newline at end of file diff -Nru tvdb-api-1.10/tvdb_api.egg-info/top_level.txt tvdb-api-2.0/tvdb_api.egg-info/top_level.txt --- tvdb-api-1.10/tvdb_api.egg-info/top_level.txt 2014-11-08 13:41:03.000000000 +0000 +++ tvdb-api-2.0/tvdb_api.egg-info/top_level.txt 2017-09-16 10:32:01.000000000 +0000 @@ -1,4 +1,3 @@ tvdb_api -tvdb_ui tvdb_exceptions -tvdb_cache +tvdb_ui diff -Nru tvdb-api-1.10/tvdb_api.py tvdb-api-2.0/tvdb_api.py --- tvdb-api-1.10/tvdb_api.py 2014-11-08 13:38:38.000000000 +0000 +++ tvdb-api-2.0/tvdb_api.py 2017-09-16 10:23:26.000000000 +0000 @@ -1,9 +1,9 @@ #!/usr/bin/env python -#encoding:utf-8 -#author:dbr/Ben -#project:tvdb_api -#repository:http://github.com/dbr/tvdb_api -#license:unlicense (http://unlicense.org/) +# encoding:utf-8 +# author:dbr/Ben +# project:tvdb_api +# repository:http://github.com/dbr/tvdb_api +# license:unlicense (http://unlicense.org/) """Simple-to-use Python interface to The TVDB's API (thetvdb.com) @@ -11,42 +11,38 @@ >>> from tvdb_api import Tvdb >>> t = Tvdb() ->>> t['Lost'][4][11]['episodename'] +>>> t['Lost'][4][11]['episodeName'] u'Cabin Fever' """ -__author__ = "dbr/Ben" -__version__ = "1.10" -import sys +__author__ = "dbr/Ben" +__version__ = "2.0" -IS_PY2 = sys.version_info[0] == 2 +import sys import os import time -if IS_PY2: - import urllib - import urllib2 - from tvdb_cache import CacheHandler - from urllib import quote as url_quote -else: - import requests - from urllib.parse import quote as url_quote +import types import getpass import tempfile import warnings import logging import datetime -import zipfile +import hashlib -try: - import xml.etree.cElementTree as ElementTree -except ImportError: - import xml.etree.ElementTree as ElementTree - -try: - import gzip -except ImportError: - gzip = None +import requests +import requests_cache +from requests_cache.backends.base import _to_bytes, _DEFAULT_HEADERS + + +IS_PY2 = sys.version_info[0] == 2 + +if IS_PY2: + user_input = raw_input + from urllib import quote as url_quote +else: + from urllib.parse import quote as url_quote + user_input = input if IS_PY2: @@ -56,17 +52,217 @@ int_types = int text_type = str - -from tvdb_ui import BaseUI, ConsoleUI -from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound, - tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound) - lastTimeout = None + def log(): return logging.getLogger("tvdb_api") +## Exceptions + +class tvdb_exception(Exception): + """Any exception generated by tvdb_api + """ + pass + +class tvdb_error(tvdb_exception): + """An error with thetvdb.com (Cannot connect, for example) + """ + pass + +class tvdb_userabort(tvdb_exception): + """User aborted the interactive selection (via + the q command, ^c etc) + """ + pass + +class tvdb_notauthorized(tvdb_exception): + """An authorization error with thetvdb.com + """ + pass + +class tvdb_shownotfound(tvdb_exception): + """Show cannot be found on thetvdb.com (non-existant show) + """ + pass + +class tvdb_seasonnotfound(tvdb_exception): + """Season cannot be found on thetvdb.com + """ + pass + +class tvdb_episodenotfound(tvdb_exception): + """Episode cannot be found on thetvdb.com + """ + pass + +class tvdb_resourcenotfound(tvdb_exception): + """Resource cannot be found on thetvdb.com + """ + pass + +class tvdb_invalidlanguage(tvdb_exception): + """invalid language given on thetvdb.com + """ + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + +class tvdb_attributenotfound(tvdb_exception): + """Raised if an episode does not have the requested + attribute (such as a episode name) + """ + pass + + +## UI + +class BaseUI(object): + """Base user interface for Tvdb show selection. + + Selects first show. + + A UI is a callback. A class, it's __init__ function takes two arguments: + + - config, which is the Tvdb config dict, setup in tvdb_api.py + - log, which is Tvdb's logger instance (which uses the logging module). You can + call log.info() log.warning() etc + + It must have a method "selectSeries", this is passed a list of dicts, each dict + contains the the keys "name" (human readable show name), and "sid" (the shows + ID as on thetvdb.com). For example: + + [{'name': u'Lost', 'sid': u'73739'}, + {'name': u'Lost Universe', 'sid': u'73181'}] + + The "selectSeries" method must return the appropriate dict, or it can raise + tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show + cannot be found). + + A simple example callback, which returns a random series: + + >>> import random + >>> from tvdb_ui import BaseUI + >>> class RandomUI(BaseUI): + ... def selectSeries(self, allSeries): + ... import random + ... return random.choice(allSeries) + + Then to use it.. + + >>> from tvdb_api import Tvdb + >>> t = Tvdb(custom_ui = RandomUI) + >>> random_matching_series = t['Lost'] + >>> type(random_matching_series) + + """ + def __init__(self, config, log = None): + self.config = config + if log is not None: + warnings.warn("the UI's log parameter is deprecated, instead use\n" + "use import logging; logging.getLogger('ui').info('blah')\n" + "The self.log attribute will be removed in the next version") + self.log = logging.getLogger(__name__) + + def selectSeries(self, allSeries): + return allSeries[0] + + +class ConsoleUI(BaseUI): + """Interactively allows the user to select a show from a console based UI + """ + + def _displaySeries(self, allSeries, limit = 6): + """Helper function, lists series with corresponding ID + """ + if limit is not None: + toshow = allSeries[:limit] + else: + toshow = allSeries + + print("TVDB Search Results:") + for i, cshow in enumerate(toshow): + i_show = i + 1 # Start at more human readable number 1 (not 0) + log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesName'])) + if i == 0: + extra = " (default)" + else: + extra = "" + + lid_map = dict((v, k) for (k, v) in self.config['langabbv_to_id'].items()) + + output = "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % ( + i_show, + cshow['seriesName'], + lid_map[cshow['lid']], + str(cshow['id']), + cshow['lid'], + extra + ) + if IS_PY2: + print(output.encode("UTF-8", "ignore")) + else: + print(output) + + def selectSeries(self, allSeries): + self._displaySeries(allSeries) + + if len(allSeries) == 1: + # Single result, return it! + print("Automatically selecting only result") + return allSeries[0] + + if self.config['select_first'] is True: + print("Automatically returning first search result") + return allSeries[0] + + while True: # return breaks this loop + try: + print("Enter choice (first number, return for default, 'all', ? for help):") + ans = user_input() + except KeyboardInterrupt: + raise tvdb_userabort("User aborted (^c keyboard interupt)") + except EOFError: + raise tvdb_userabort("User aborted (EOF received)") + + log().debug('Got choice of: %s' % (ans)) + try: + selected_id = int(ans) - 1 # The human entered 1 as first result, not zero + except ValueError: # Input was not number + if len(ans.strip()) == 0: + # Default option + log().debug('Default option, returning first series') + return allSeries[0] + if ans == "q": + log().debug('Got quit command (q)') + raise tvdb_userabort("User aborted ('q' quit command)") + elif ans == "?": + print("## Help") + print("# Enter the number that corresponds to the correct show.") + print("# a - display all results") + print("# all - display all results") + print("# ? - this help") + print("# q - abort tvnamer") + print("# Press return with no input to select first result") + elif ans.lower() in ["a", "all"]: + self._displaySeries(allSeries, limit = None) + else: + log().debug('Unknown keypress %s' % (ans)) + else: + log().debug('Trying to return ID: %d' % (selected_id)) + try: + return allSeries[selected_id] + except IndexError: + log().debug('Invalid show number entered!') + print("Invalid number (%s) selected!") + self._displaySeries(allSeries) + + +## Main API + class ShowContainer(dict): """Simple dict that holds a series of Show instances """ @@ -78,7 +274,7 @@ def __setitem__(self, key, value): self._stack.append(key) - #keep only the 100th latest results + # keep only the 100th latest results if time.time() - self._lastgc > 20: for o in self._stack[:-100]: del self[o] @@ -97,12 +293,22 @@ self.data = {} def __repr__(self): - return "" % ( - self.data.get(u'seriesname', 'instance'), + return "" % ( + self.data.get(u'seriesName', 'instance'), len(self) ) def __getitem__(self, key): + v1_compatibility = { + 'seriesname': 'seriesName', + } + + if key in v1_compatibility: + import warnings + msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % ( + key, v1_compatibility[key]) + key = v1_compatibility[key] + if key in self: # Key is an episode, return it return dict.__getitem__(self, key) @@ -114,22 +320,34 @@ # Data wasn't found, raise appropriate error if isinstance(key, int) or key.isdigit(): # Episode number x was not found - raise tvdb_seasonnotfound("Could not find season %s" % (repr(key))) + raise tvdb_seasonnotfound( + "Could not find season %s" % (repr(key)) + ) else: # If it's not numeric, it must be an attribute name, which # doesn't exist, so attribute error. - raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) + raise tvdb_attributenotfound( + "Cannot find attribute %s" % (repr(key)) + ) def airedOn(self, date): - ret = self.search(str(date), 'firstaired') + """Deprecated: use aired_on instead + """ + warnings.warn("Show.airedOn method renamed to aired_on", category=DeprecationWarning) + return self.aired_on(date) + + def aired_on(self, date): + ret = self.search(str(date), 'firstAired') if len(ret) == 0: - raise tvdb_episodenotfound("Could not find any episodes that aired on %s" % date) + raise tvdb_episodenotfound( + "Could not find any episodes that aired on %s" % date + ) return ret - def search(self, term = None, key = None): + def search(self, term=None, key=None): """ - Search all episodes in show. Can search all data, or a specific key (for - example, episodename) + Search all episodes in show. Can search all data, or a specific key + (for example, episodename) Always returns an array (can be empty). First index contains the first match, and so on. @@ -141,9 +359,9 @@ Search terms are converted to lower case (unicode) strings. # Examples - + These examples assume t is an instance of Tvdb(): - + >>> t = Tvdb() >>> @@ -151,27 +369,27 @@ containing "my first day": >>> t['Scrubs'].search("my first day") - [] + [] >>> Search for "My Name Is Earl" episode named "Faked His Own Death": - >>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename') - [] + >>> t['My Name Is Earl'].search('Faked My Own Death', key='episodeName') + [] >>> To search Scrubs for all episodes with "mentor" in the episode name: - >>> t['scrubs'].search('mentor', key = 'episodename') - [, ] + >>> t['scrubs'].search('mentor', key='episodeName') + [, ] >>> # Using search results >>> results = t['Scrubs'].search("my first") - >>> print results[0]['episodename'] + >>> print results[0]['episodeName'] My First Day - >>> for x in results: print x['episodename'] + >>> for x in results: print x['episodeName'] My First Day My First Step My First Kill @@ -179,7 +397,7 @@ """ results = [] for cur_season in self.values(): - searchresult = cur_season.search(term = term, key = key) + searchresult = cur_season.search(term=term, key=key) if len(searchresult) != 0: results.extend(searchresult) @@ -187,7 +405,7 @@ class Season(dict): - def __init__(self, show = None): + def __init__(self, show=None): """The show attribute points to the parent show """ self.show = show @@ -203,20 +421,20 @@ else: return dict.__getitem__(self, episode_number) - def search(self, term = None, key = None): + def search(self, term=None, key=None): """Search all episodes in season, returns a list of matching Episode instances. >>> t = Tvdb() >>> t['scrubs'][1].search('first day') - [] + [] >>> See Show.search documentation for further information on search """ results = [] for ep in self.values(): - searchresult = ep.search(term = term, key = key) + searchresult = ep.search(term=term, key=key) if searchresult is not None: results.append( searchresult @@ -225,17 +443,17 @@ class Episode(dict): - def __init__(self, season = None): + def __init__(self, season=None): """The season attribute points to the parent season """ self.season = season def __repr__(self): - seasno = int(self.get(u'seasonnumber', 0)) - epno = int(self.get(u'episodenumber', 0)) - epname = self.get(u'episodename') + seasno = self.get(u'airedSeason', 0) + epno = self.get(u'airedEpisodeNumber', 0) + epname = self.get(u'episodeName') if epname is not None: - return "" % (seasno, epno, epname) + return "" % (seasno, epno, epname) else: return "" % (seasno, epno) @@ -243,40 +461,62 @@ try: return dict.__getitem__(self, key) except KeyError: - raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) + v1_compatibility = { + 'episodenumber': 'airedEpisodeNumber', + 'firstaired': 'firstAired', + 'seasonnumber': 'airedSeason', + 'episodename': 'episodeName', + } + if key in v1_compatibility: + msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % ( + key, v1_compatibility[key]) + warnings.warn(msg, category=DeprecationWarning) + try: + value = dict.__getitem__(self, v1_compatibility[key]) + if key in ['episodenumber', 'seasonnumber']: + # This was a string in v1 + return str(value) + else: + return value + except KeyError: + # We either return something or we get the exception below + pass - def search(self, term = None, key = None): + raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) + + def search(self, term=None, key=None): """Search episode data for term, if it matches, return the Episode (self). The key parameter can be used to limit the search to a specific element, for example, episodename. - + This primarily for use use by Show.search and Season.search. See Show.search for further information on search Simple example: >>> e = Episode() - >>> e['episodename'] = "An Example" + >>> e['episodeName'] = "An Example" >>> e.search("examp") - + >>> Limiting by key: - >>> e.search("examp", key = "episodename") - + >>> e.search("examp", key = "episodeName") + >>> """ - if term == None: + if term is None: raise TypeError("must supply string to search for (contents)") term = text_type(term).lower() for cur_key, cur_value in self.items(): - cur_key, cur_value = text_type(cur_key).lower(), text_type(cur_value).lower() + cur_key = text_type(cur_key) + cur_value = text_type(cur_value).lower() if key is not None and cur_key != key: # Do not search this key continue - if cur_value.find( text_type(term).lower() ) > -1: + if cur_value.find(text_type(term)) > -1: return self @@ -296,29 +536,65 @@ sortorder """ def __repr__(self): - return "" % (self.get("name")) + return "" % self.get("name") + + +def create_key(self, request): + """A new cache_key algo is required as the authentication token + changes with each run. Also there are other header params which + also change with each request (e.g. timestamp). Excluding all + headers means that Accept-Language is excluded which means + different language requests will return the cached response from + the wrong language. + + The _loadurl part checks the cache before a get is performed so + that an auth token can be obtained. If the response is already in + the cache, the auth token is not required. This prevents the need + to do a get which may access the host and fail because the session + is not yet not authorized. It is not necessary to authorize if the + cache is to be used thus saving host and network traffic. + """ + + if self._ignored_parameters: + url, body = self._remove_ignored_parameters(request) + else: + url, body = request.url, request.body + key = hashlib.sha256() + key.update(_to_bytes(request.method.upper())) + key.update(_to_bytes(url)) + if request.body: + key.update(_to_bytes(body)) + else: + if self._include_get_headers and request.headers != _DEFAULT_HEADERS: + for name, value in sorted(request.headers.items()): + # include only Accept-Language as it is important for context + if name in ['Accept-Language']: + key.update(_to_bytes(name)) + key.update(_to_bytes(value)) + return key.hexdigest() class Tvdb: """Create easy-to-use interface to name of season/episode name >>> t = Tvdb() - >>> t['Scrubs'][1][24]['episodename'] + >>> t['Scrubs'][1][24]['episodeName'] u'My Last Day' """ def __init__(self, - interactive = False, - select_first = False, - debug = False, - cache = True, - banners = False, - actors = False, - custom_ui = None, - language = None, - search_all_languages = False, - apikey = None, - forceConnect=False, - useZip=False, - dvdorder=False): + interactive=False, + select_first=False, + debug=False, + cache=True, + banners=False, + actors=False, + custom_ui=None, + language=None, + search_all_languages=False, + apikey=None, + username=None, + userkey=None, + forceConnect=False, + dvdorder=False): """interactive (True/False): When True, uses built-in console UI is used to select the correct show. @@ -335,26 +611,22 @@ >>> import logging >>> logging.basicConfig(level = logging.DEBUG) - cache (True/False/str/unicode/urllib2 opener): - Retrieved XML are persisted to to disc. If true, stores in - tvdb_api folder under your systems TEMP_DIR, if set to - str/unicode instance it will use this as the cache - location. If False, disables caching. Can also be passed - an arbitrary Python object, which is used as a urllib2 - opener, which should be created by urllib2.build_opener - - In Python 3, True/False enable or disable default - caching. Passing string specified directory where to store - the "tvdb.sqlite3" cache file. Also a custom + cache (True/False/str/requests_cache.CachedSession): + + Retrieved URLs can be persisted to to disc. + + True/False enable or disable default caching. Passing + string specifies the directory where to store the + "tvdb.sqlite3" cache file. Alternatively a custom requests.Session instance can be passed (e.g maybe a - customised instance of requests_cache.CachedSession) + customised instance of `requests_cache.CachedSession`) banners (True/False): Retrieves the banners for a show. These are accessed via the _banners key of a Show(), for example: >>> Tvdb(banners=True)['scrubs']['_banners'].keys() - ['fanart', 'poster', 'series', 'season'] + [u'fanart', u'poster', u'seasonwide', u'season', u'series'] actors (True/False): Retrieves a list of the actors for a show. These are accessed @@ -362,7 +634,7 @@ >>> t = Tvdb(actors=True) >>> t['scrubs']['_actors'][0]['name'] - u'Zach Braff' + u'John C. McGinley' custom_ui (tvdb_ui.BaseUI subclass): A callable subclass of tvdb_ui.BaseUI (overrides interactive option) @@ -378,7 +650,7 @@ By default, Tvdb will only search in the language specified using the language option. When this is True, it will search for the show in and language - + apikey (str/unicode): Override the default thetvdb.com API key. By default it will use tvdb_api's own key (fine for small scripts), but you can use your @@ -386,129 +658,123 @@ tvdb_api in a larger application) See http://thetvdb.com/?tab=apiregister to get your own key + username (str/unicode): + Override the default thetvdb.com username. By default it will use + tvdb_api's own username (fine for small scripts), but you can use your + own key if desired - this is recommended if you are embedding + tvdb_api in a larger application) + See http://thetvdb.com/ to register an account + + userkey (str/unicode): + Override the default thetvdb.com userkey. By default it will use + tvdb_api's own userkey (fine for small scripts), but you can use your + own key if desired - this is recommended if you are embedding + tvdb_api in a larger application) + See http://thetvdb.com/ to register an account + forceConnect (bool): If true it will always try to connect to theTVDB.com even if we recently timed out. By default it will wait one minute before trying again, and any requests within that one minute window will return an exception immediately. - - useZip (bool): - Download the zip archive where possibale, instead of the xml. - This is only used when all episodes are pulled. - And only the main language xml is used, the actor and banner xml are lost. """ - global lastTimeout - + # if we're given a lastTimeout that is less than 1 min just give up - if not forceConnect and lastTimeout != None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1): + if not forceConnect and lastTimeout is not None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1): raise tvdb_error("We recently timed out, so giving up early this time") - - self.shows = ShowContainer() # Holds all Show classes - self.corrections = {} # Holds show-name to show_id mapping + + self.shows = ShowContainer() # Holds all Show classes + self.corrections = {} # Holds show-name to show_id mapping self.config = {} - if apikey is not None: - self.config['apikey'] = apikey + if apikey and username and userkey: + self.config['auth_payload'] = { + "apikey": apikey, + "username": username, + "userkey": userkey + } else: - self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key + self.config['auth_payload'] = { + "apikey": "0629B785CE550C8D", + "userkey": "", + "username": "" + } - self.config['debug_enabled'] = debug # show debugging messages + self.config['debug_enabled'] = debug # show debugging messages self.config['custom_ui'] = custom_ui - self.config['interactive'] = interactive # prompt for correct series? + self.config['interactive'] = interactive # prompt for correct series? self.config['select_first'] = select_first self.config['search_all_languages'] = search_all_languages - self.config['useZip'] = useZip - self.config['dvdorder'] = dvdorder - if not IS_PY2: # FIXME: Allow using requests in Python 2? - import requests_cache - if cache is True: - self.session = requests_cache.CachedSession( - expire_after=21600, # 6 hours - backend='sqlite', - cache_name=self._getTempDir(), - ) - self.config['cache_enabled'] = True - elif cache is False: - self.session = requests.Session() - self.config['cache_enabled'] = False - elif isinstance(cache, text_type): - # Specified cache path - self.session = requests_cache.CachedSession( - expire_after=21600, # 6 hours - backend='sqlite', - cache_name=os.path.join(cache, "tvdb_api"), - ) - else: - self.session = cache - try: - self.session.get - except AttributeError: - raise ValueError("cache argument must be True/False, string as cache path or requests.Session-type object (e.g from requests_cache.CachedSession)") - else: - # For backwards compatibility in Python 2.x - if cache is True: - self.config['cache_enabled'] = True - self.config['cache_location'] = self._getTempDir() - self.urlopener = urllib2.build_opener( - CacheHandler(self.config['cache_location']) + if cache is True: + self.session = requests_cache.CachedSession( + expire_after=21600, # 6 hours + backend='sqlite', + cache_name=self._getTempDir(), + include_get_headers=True ) - - elif cache is False: - self.config['cache_enabled'] = False - self.urlopener = urllib2.build_opener() # default opener with no caching - - elif isinstance(cache, basestring): - self.config['cache_enabled'] = True - self.config['cache_location'] = cache - self.urlopener = urllib2.build_opener( - CacheHandler(self.config['cache_location']) + self.session.cache.create_key = types.MethodType(create_key, self.session.cache) + self.session.remove_expired_responses() + self.config['cache_enabled'] = True + elif cache is False: + self.session = requests.Session() + self.config['cache_enabled'] = False + elif isinstance(cache, str): + # Specified cache path + self.session = requests_cache.CachedSession( + expire_after=21600, # 6 hours + backend='sqlite', + cache_name=os.path.join(cache, "tvdb_api"), + include_get_headers=True ) - - elif isinstance(cache, urllib2.OpenerDirector): - # If passed something from urllib2.build_opener, use that - log().debug("Using %r as urlopener" % cache) - self.config['cache_enabled'] = True - self.urlopener = cache - - else: - raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache))) + self.session.cache.create_key = types.MethodType(create_key, self.session.cache) + self.session.remove_expired_responses() + else: + self.session = cache + try: + self.session.get + except AttributeError: + raise ValueError("cache argument must be True/False, string as cache path or requests.Session-type object (e.g from requests_cache.CachedSession)") self.config['banners_enabled'] = banners self.config['actors_enabled'] = actors if self.config['debug_enabled']: - warnings.warn("The debug argument to tvdb_api.__init__ will be removed in the next version. " - "To enable debug messages, use the following code before importing: " - "import logging; logging.basicConfig(level=logging.DEBUG)") + warnings.warn( + "The debug argument to tvdb_api.__init__ will be removed in the next version. " + "To enable debug messages, use the following code before importing: " + "import logging; logging.basicConfig(level=logging.DEBUG)" + ) logging.basicConfig(level=logging.DEBUG) - # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml self.config['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" + "da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr", + "ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv", + "no" ] # thetvdb.com should be based around numeric language codes, # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16 # requires the language ID, thus this mapping is required (mainly # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations) - self.config['langabbv_to_id'] = {'el': 20, 'en': 7, 'zh': 27, - 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, - 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, - 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30} + self.config['langabbv_to_id'] = { + 'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, + 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, + 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, + 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30 + } if language is None: self.config['language'] = 'en' @@ -523,21 +789,22 @@ # The following url_ configs are based of the # http://thetvdb.com/wiki/index.php/Programmers_API self.config['base_url'] = "http://thetvdb.com" + self.config['api_url'] = "https://api.thetvdb.com" - if self.config['search_all_languages']: - self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config - else: - self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % self.config + self.config['url_getSeries'] = u"%(api_url)s/search/series?name=%%s" % self.config - self.config['url_epInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml" % self.config - self.config['url_epInfo_zip'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip" % self.config + self.config['url_epInfo'] = u"%(api_url)s/series/%%s/episodes" % self.config - self.config['url_seriesInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml" % self.config - self.config['url_actorsInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config + self.config['url_seriesInfo'] = u"%(api_url)s/series/%%s" % self.config + self.config['url_actorsInfo'] = u"%(api_url)s/series/%%s/actors" % self.config - self.config['url_seriesBanner'] = u"%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config + self.config['url_seriesBanner'] = u"%(api_url)s/series/%%s/images" % self.config + self.config['url_seriesBannerInfo'] = u"%(api_url)s/series/%%s/images/query?keyType=%%s" % self.config self.config['url_artworkPrefix'] = u"%(base_url)s/banners/%%s" % self.config + self.__authorized = False + self.headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Accept-Language': self.config['language']} + def _getTempDir(self): """Returns the [system temp dir]/tvdb_api-u501 (or tvdb_api-myuser) @@ -553,101 +820,89 @@ return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid)) - def _loadUrl(self, url, recache = False, language=None): - if not IS_PY2: - # Python 3: return content at URL as bytes - resp = self.session.get(url) - if 'application/zip' in resp.headers.get("Content-Type", ''): - try: - # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20] - log().debug("We recived a zip file unpacking now ...") - from io import BytesIO - myzipfile = zipfile.ZipFile(BytesIO(resp.content)) - return myzipfile.read('%s.xml' % language) - except zipfile.BadZipfile: - self.session.cache.delete_url(url) - raise tvdb_error("Bad zip file received from thetvdb.com, could not read it") - return resp.content + def _loadUrl(self, url, data=None, recache=False, language=None): + """Return response from The TVDB API""" - else: - global lastTimeout + if not language: + language = self.config['language'] + if language not in self.config['valid_languages']: + raise ValueError("Invalid language %s, options are: %s" % ( + language, self.config['valid_languages'] + )) + self.headers['Accept-Language'] = language + + # TODO: обрабатывать исключения (Handle Exceptions) + # TODO: обновлять токен (Update Token) + # encoded url is used for hashing in the cache so + # python 2 and 3 generate the same hash + if not self.__authorized: + # only authorize of we haven't before and we + # don't have the url in the cache + fake_session_for_key = requests.Session() + fake_session_for_key.headers['Accept-Language'] = language + cache_key = None try: - log().debug("Retrieving URL %s" % url) - resp = self.urlopener.open(url) - if 'x-local-cache' in resp.headers: - log().debug("URL %s was cached in %s" % ( - url, - resp.headers['x-local-cache']) - ) - if recache: - log().debug("Attempting to recache %s" % url) - resp.recache() - except (IOError, urllib2.URLError) as errormsg: - if not str(errormsg).startswith('HTTP Error'): - lastTimeout = datetime.datetime.now() - raise tvdb_error("Could not connect to server: %s" % (errormsg)) - - - # handle gzipped content, - # http://dbr.lighthouseapp.com/projects/13342/tickets/72-gzipped-data-patch - if 'gzip' in resp.headers.get("Content-Encoding", ''): - if gzip: - from StringIO import StringIO - stream = StringIO(resp.read()) - gz = gzip.GzipFile(fileobj=stream) - return gz.read() + # in case the session class has no cache object, fail gracefully + cache_key = self.session.cache.create_key(fake_session_for_key.prepare_request(requests.Request('GET', url))) + except: + pass + if not cache_key or not self.session.cache.has_key(cache_key): + self.authorize() + + response = self.session.get(url, headers=self.headers) + r = response.json() + log().debug("loadurl: %s lid=%s" % (url, language)) + log().debug("response:") + log().debug(r) + error = r.get('Error') + errors = r.get('errors') + r_data = r.get('data') + links = r.get('links') + + if error: + if error == u'Resource not found': + # raise(tvdb_resourcenotfound) + # handle no data at a different level so it is more specific + pass + if error == u'Not Authorized': + raise(tvdb_notauthorized) + if errors: + if u'invalidLanguage' in errors: + # raise(tvdb_invalidlanguage(errors[u'invalidLanguage'])) + # invalidLanguage does not mean there is no data + # there is just less data + pass - raise tvdb_error("Received gzip data from thetvdb.com, but could not correctly handle it") + if data and isinstance(data, list): + data.extend(r_data) + else: + data = r_data - if 'application/zip' in resp.headers.get("Content-Type", ''): - try: - # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20] - log().debug("We recived a zip file unpacking now ...") - from StringIO import StringIO - zipdata = StringIO() - zipdata.write(resp.read()) - myzipfile = zipfile.ZipFile(zipdata) - return myzipfile.read('%s.xml' % language) - except zipfile.BadZipfile: - if 'x-local-cache' in resp.headers: - resp.delete_cache() - raise tvdb_error("Bad zip file received from thetvdb.com, could not read it") + if links and links['next']: + url = url.split('?')[0] + _url = url + "?page=%s" % links['next'] + self._loadUrl(_url, data) + + return data - return resp.read() + def authorize(self): + log().debug("auth") + r = self.session.post('https://api.thetvdb.com/login', json=self.config['auth_payload'], headers=self.headers) + r_json = r.json() + error = r_json.get('Error') + if error: + if error == u'Not Authorized': + raise(tvdb_notauthorized) + token = r_json.get('token') + self.headers['Authorization'] = "Bearer %s" % text_type(token) + self.__authorized = True def _getetsrc(self, url, language=None): """Loads a URL using caching, returns an ElementTree of the source """ src = self._loadUrl(url, language=language) - - # TVDB doesn't sanitize \r (CR) from user input in some fields, - # remove it to avoid errors. Change from SickBeard, from will14m - if not IS_PY2: - # Remove trailing \r byte - src = src.replace(b"\r", b"") - else: - src = src.rstrip("\r") # FIXME: this seems wrong - - try: - return ElementTree.fromstring(src) - except SyntaxError: - src = self._loadUrl(url, recache=True, language=language) - try: - return ElementTree.fromstring(src) - except SyntaxError as exceptionmsg: - errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % ( - exceptionmsg - ) - - if self.config['cache_enabled']: - errormsg += "\nFirst try emptying the cache folder at..\n%s" % ( - self.config['cache_location'] - ) - - errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on" - errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n" - raise tvdb_error(errormsg) + return src def _setItem(self, sid, seas, ep, attrib, value): """Creates a new episode, creating Show(), Season() and @@ -667,9 +922,9 @@ if sid not in self.shows: self.shows[sid] = Show() if seas not in self.shows[sid]: - self.shows[sid][seas] = Season(show = self.shows[sid]) + self.shows[sid][seas] = Season(show=self.shows[sid]) if ep not in self.shows[sid][seas]: - self.shows[sid][seas][ep] = Episode(season = self.shows[sid][seas]) + self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas]) self.shows[sid][seas][ep][attrib] = value def _setShowData(self, sid, key, value): @@ -679,17 +934,6 @@ self.shows[sid] = Show() self.shows[sid].data[key] = value - def _cleanData(self, data): - """Cleans up strings returned by TheTVDB.com - - Issues corrected: - - Replaces & with & - - Trailing whitespace - """ - data = data.replace(u"&", u"&") - data = data.strip() - return data - def search(self, series): """This searches TheTVDB.com for the series name and returns the result list @@ -697,16 +941,17 @@ series = url_quote(series.encode("utf-8")) log().debug("Searching for show %s" % series) seriesEt = self._getetsrc(self.config['url_getSeries'] % (series)) + if not seriesEt: + log().debug('Series result returned zero') + raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)") + allSeries = [] for series in seriesEt: - result = dict((k.tag.lower(), k.text) for k in series.getchildren()) - result['id'] = int(result['id']) - result['lid'] = self.config['langabbv_to_id'][result['language']] - if 'aliasnames' in result: - result['aliasnames'] = result['aliasnames'].split("|") - log().debug('Found series %(seriesname)s' % result) - allSeries.append(result) - + series['lid'] = self.config['langabbv_to_id'][self.config['language']] + series['language'] = self.config['language'] + log().debug('Found series %(seriesName)s' % series) + allSeries.append(series) + return allSeries def _getSeries(self, series): @@ -717,20 +962,16 @@ """ allSeries = self.search(series) - if len(allSeries) == 0: - log().debug('Series result returned zero') - raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)") - if self.config['custom_ui'] is not None: log().debug("Using custom UI %s" % (repr(self.config['custom_ui']))) - ui = self.config['custom_ui'](config = self.config) + ui = self.config['custom_ui'](config=self.config) else: if not self.config['interactive']: log().debug('Auto-selecting first search result using BaseUI') - ui = BaseUI(config = self.config) + ui = BaseUI(config=self.config) else: log().debug('Interactively selecting show using ConsoleUI') - ui = ConsoleUI(config = self.config) + ui = ConsoleUI(config=self.config) return ui.selectSeries(allSeries) @@ -742,8 +983,8 @@ >>> t = Tvdb(banners = True) >>> t['scrubs']['_banners'].keys() - ['fanart', 'poster', 'series', 'season'] - >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath'] + [u'fanart', u'poster', u'seasonwide', u'season', u'series'] + >>> t['scrubs']['_banners']['poster']['680x1000'][35308]['_bannerpath'] u'http://thetvdb.com/banners/posters/76156-2.jpg' >>> @@ -753,38 +994,37 @@ This interface will be improved in future versions. """ log().debug('Getting season banners for %s' % (sid)) - bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) ) + bannersEt = self._getetsrc(self.config['url_seriesBanner'] % sid) banners = {} - for cur_banner in bannersEt.findall('Banner'): - bid = cur_banner.find('id').text - btype = cur_banner.find('BannerType') - btype2 = cur_banner.find('BannerType2') - if btype is None or btype2 is None: - continue - btype, btype2 = btype.text, btype2.text - if not btype in banners: - banners[btype] = {} - if not btype2 in banners[btype]: - banners[btype][btype2] = {} - if not bid in banners[btype][btype2]: - banners[btype][btype2][bid] = {} - - for cur_element in cur_banner.getchildren(): - tag = cur_element.tag.lower() - value = cur_element.text - if tag is None or value is None: + for cur_banner in bannersEt.keys(): + banners_info = self._getetsrc(self.config['url_seriesBannerInfo'] % (sid, cur_banner)) + for banner_info in banners_info: + bid = banner_info.get('id') + btype = banner_info.get('keyType') + btype2 = banner_info.get('resolution') + if btype is None or btype2 is None: continue - tag, value = tag.lower(), value.lower() - banners[btype][btype2][bid][tag] = value - for k, v in list(banners[btype][btype2][bid].items()): - if k.endswith("path"): - new_key = "_%s" % (k) - log().debug("Transforming %s to %s" % (k, new_key)) - new_url = self.config['url_artworkPrefix'] % (v) - banners[btype][btype2][bid][new_key] = new_url + if btype not in banners: + banners[btype] = {} + if btype2 not in banners[btype]: + banners[btype][btype2] = {} + if bid not in banners[btype][btype2]: + banners[btype][btype2][bid] = {} + + banners[btype][btype2][bid]['bannerpath'] = banner_info['fileName'] + banners[btype][btype2][bid]['resolution'] = banner_info['resolution'] + banners[btype][btype2][bid]['subKey'] = banner_info['subKey'] + + for k, v in list(banners[btype][btype2][bid].items()): + if k.endswith("path"): + new_key = "_%s" % k + log().debug("Transforming %s to %s" % (k, new_key)) + new_url = self.config['url_artworkPrefix'] % v + banners[btype][btype2][bid][new_key] = new_url - self._setShowData(sid, "_banners", banners) + banners[btype]['raw'] = banners_info + self._setShowData(sid, "_banners", banners) def _parseActors(self, sid): """Parsers actors XML, from @@ -799,13 +1039,13 @@ >>> type(actors[0]) >>> actors[0] - + >>> sorted(actors[0].keys()) - ['id', 'image', 'name', 'role', 'sortorder'] + [u'id', u'image', u'imageAdded', u'imageAuthor', u'lastUpdated', u'name', u'role', u'seriesId', u'sortOrder'] >>> actors[0]['name'] - u'Zach Braff' + u'John C. McGinley' >>> actors[0]['image'] - u'http://thetvdb.com/banners/actors/43640.jpg' + u'http://thetvdb.com/banners/actors/43638.jpg' Any key starting with an underscore has been processed (not the raw data from the XML) @@ -814,16 +1054,14 @@ actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid)) cur_actors = Actors() - for curActorItem in actorsEt.findall("Actor"): + for curActorItem in actorsEt: curActor = Actor() - for curInfo in curActorItem: - tag = curInfo.tag.lower() - value = curInfo.text + for curInfo in curActorItem.keys(): + tag = curInfo + value = curActorItem[curInfo] if value is not None: if tag == "image": value = self.config['url_artworkPrefix'] % (value) - else: - value = self._cleanData(value) curActor[tag] = value cur_actors.append(curActor) self._setShowData(sid, '_actors', cur_actors) @@ -838,7 +1076,6 @@ log().debug('Config language is none, using show language') if language is None: raise tvdb_error("config['language'] was None, this should not happen") - getShowInLanguage = language else: log().debug( 'Configured language %s override show language of %s' % ( @@ -846,24 +1083,23 @@ language ) ) - getShowInLanguage = self.config['language'] # Parse show information log().debug('Getting all series data for %s' % (sid)) seriesInfoEt = self._getetsrc( - self.config['url_seriesInfo'] % (sid, getShowInLanguage) + self.config['url_seriesInfo'] % sid ) - for curInfo in seriesInfoEt.findall("Series")[0]: - tag = curInfo.tag.lower() - value = curInfo.text + for curInfo in seriesInfoEt.keys(): + tag = curInfo + value = seriesInfoEt[curInfo] if value is not None: if tag in ['banner', 'fanart', 'poster']: value = self.config['url_artworkPrefix'] % (value) - else: - value = self._cleanData(value) self._setShowData(sid, tag, value) + # set language + self._setShowData(sid, u'language', self.config['language']) # Parse banners if self.config['banners_enabled']: @@ -876,47 +1112,42 @@ # Parse episode data log().debug('Getting all episodes of %s' % (sid)) - if self.config['useZip']: - url = self.config['url_epInfo_zip'] % (sid, language) - else: - url = self.config['url_epInfo'] % (sid, language) + url = self.config['url_epInfo'] % sid - epsEt = self._getetsrc( url, language=language) + epsEt = self._getetsrc(url, language=language) - for cur_ep in epsEt.findall("Episode"): + for cur_ep in epsEt: if self.config['dvdorder']: log().debug('Using DVD ordering.') - use_dvd = cur_ep.find('DVD_season').text != None and cur_ep.find('DVD_episodenumber').text != None + use_dvd = cur_ep.get('dvdSeason') is not None and cur_ep.get('dvdEpisodeNumber') is not None else: use_dvd = False if use_dvd: - elem_seasnum, elem_epno = cur_ep.find('DVD_season'), cur_ep.find('DVD_episodenumber') + elem_seasnum, elem_epno = cur_ep.get('dvdSeason'), cur_ep.get('dvdEpisodeNumber') else: - elem_seasnum, elem_epno = cur_ep.find('SeasonNumber'), cur_ep.find('EpisodeNumber') + elem_seasnum, elem_epno = cur_ep['airedSeason'], cur_ep['airedEpisodeNumber'] if elem_seasnum is None or elem_epno is None: log().warning("An episode has incomplete season/episode number (season: %r, episode: %r)" % ( elem_seasnum, elem_epno)) - log().debug( - " ".join( - "%r is %r" % (child.tag, child.text) for child in cur_ep.getchildren())) + #log().debug( + # " ".join( + # "%r is %r" % (child.tag, child.text) for child in cur_ep.getchildren())) # TODO: Should this happen? - continue # Skip to next episode + continue # Skip to next episode # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data - seas_no = int(float(elem_seasnum.text)) - ep_no = int(float(elem_epno.text)) + seas_no = elem_seasnum + ep_no = elem_epno - for cur_item in cur_ep.getchildren(): - tag = cur_item.tag.lower() - value = cur_item.text + for cur_item in cur_ep.keys(): + tag = cur_item + value = cur_ep[cur_item] if value is not None: if tag == 'filename': value = self.config['url_artworkPrefix'] % (value) - else: - value = self._cleanData(value) self._setItem(sid, seas_no, ep_no, tag, value) def _nameToSid(self, name): @@ -925,16 +1156,16 @@ the correct SID. """ if name in self.corrections: - log().debug('Correcting %s to %s' % (name, self.corrections[name]) ) + log().debug('Correcting %s to %s' % (name, self.corrections[name])) sid = self.corrections[name] else: - log().debug('Getting show %s' % (name)) - selected_series = self._getSeries( name ) - sname, sid = selected_series['seriesname'], selected_series['id'] - log().debug('Got %(seriesname)s, id %(id)s' % selected_series) + log().debug('Getting show %s' % name) + selected_series = self._getSeries(name) + sid = selected_series['id'] + log().debug('Got %(seriesName)s, id %(id)s' % selected_series) self.corrections[name] = sid - self._getShowData(selected_series['id'], selected_series['language']) + self._getShowData(selected_series['id'], self.config['language']) return sid @@ -947,14 +1178,13 @@ if key not in self.shows: self._getShowData(key, self.config['language']) return self.shows[key] - - key = key.lower() # make key lower case + sid = self._nameToSid(key) - log().debug('Got series id %s' % (sid)) + log().debug('Got series id %s' % sid) return self.shows[sid] def __repr__(self): - return str(self.shows) + return repr(self.shows) def main(): @@ -964,9 +1194,10 @@ import logging logging.basicConfig(level=logging.DEBUG) - tvdb_instance = Tvdb(interactive=True, cache=False) + tvdb_instance = Tvdb(interactive=False, cache=False) print(tvdb_instance['Lost']['seriesname']) print(tvdb_instance['Lost'][1][4]['episodename']) + if __name__ == '__main__': main() diff -Nru tvdb-api-1.10/tvdb_cache.py tvdb-api-2.0/tvdb_cache.py --- tvdb-api-1.10/tvdb_cache.py 2014-11-08 13:38:42.000000000 +0000 +++ tvdb-api-2.0/tvdb_cache.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,251 +0,0 @@ -#!/usr/bin/env python -#encoding:utf-8 -#author:dbr/Ben -#project:tvdb_api -#repository:http://github.com/dbr/tvdb_api -#license:unlicense (http://unlicense.org/) - -""" -urllib2 caching handler -Modified from http://code.activestate.com/recipes/491261/ -""" -from __future__ import with_statement - -__author__ = "dbr/Ben" -__version__ = "1.10" - -import os -import time -import errno -import httplib -import urllib2 -import StringIO -from hashlib import md5 -from threading import RLock - -cache_lock = RLock() - -def locked_function(origfunc): - """Decorator to execute function under lock""" - def wrapped(*args, **kwargs): - cache_lock.acquire() - try: - return origfunc(*args, **kwargs) - finally: - cache_lock.release() - return wrapped - -def calculate_cache_path(cache_location, url): - """Checks if [cache_location]/[hash_of_url].headers and .body exist - """ - thumb = md5(url).hexdigest() - header = os.path.join(cache_location, thumb + ".headers") - body = os.path.join(cache_location, thumb + ".body") - return header, body - -def check_cache_time(path, max_age): - """Checks if a file has been created/modified in the [last max_age] seconds. - False means the file is too old (or doesn't exist), True means it is - up-to-date and valid""" - if not os.path.isfile(path): - return False - cache_modified_time = os.stat(path).st_mtime - time_now = time.time() - if cache_modified_time < time_now - max_age: - # Cache is old - return False - else: - return True - -@locked_function -def exists_in_cache(cache_location, url, max_age): - """Returns if header AND body cache file exist (and are up-to-date)""" - hpath, bpath = calculate_cache_path(cache_location, url) - if os.path.exists(hpath) and os.path.exists(bpath): - return( - check_cache_time(hpath, max_age) - and check_cache_time(bpath, max_age) - ) - else: - # File does not exist - return False - -@locked_function -def store_in_cache(cache_location, url, response): - """Tries to store response in cache.""" - hpath, bpath = calculate_cache_path(cache_location, url) - try: - outf = open(hpath, "wb") - headers = str(response.info()) - outf.write(headers) - outf.close() - - outf = open(bpath, "wb") - outf.write(response.read()) - outf.close() - except IOError: - return True - else: - return False - -@locked_function -def delete_from_cache(cache_location, url): - """Deletes a response in cache.""" - hpath, bpath = calculate_cache_path(cache_location, url) - try: - if os.path.exists(hpath): - os.remove(hpath) - if os.path.exists(bpath): - os.remove(bpath) - except IOError: - return True - else: - return False - -class CacheHandler(urllib2.BaseHandler): - """Stores responses in a persistant on-disk cache. - - If a subsequent GET request is made for the same URL, the stored - response is returned, saving time, resources and bandwidth - """ - @locked_function - def __init__(self, cache_location, max_age = 21600): - """The location of the cache directory""" - self.max_age = max_age - self.cache_location = cache_location - if not os.path.exists(self.cache_location): - try: - os.mkdir(self.cache_location) - except OSError, e: - if e.errno == errno.EEXIST and os.path.isdir(self.cache_location): - # File exists, and it's a directory, - # another process beat us to creating this dir, that's OK. - pass - else: - # Our target dir is already a file, or different error, - # relay the error! - raise - - def default_open(self, request): - """Handles GET requests, if the response is cached it returns it - """ - if request.get_method() is not "GET": - return None # let the next handler try to handle the request - - if exists_in_cache( - self.cache_location, request.get_full_url(), self.max_age - ): - return CachedResponse( - self.cache_location, - request.get_full_url(), - set_cache_header = True - ) - else: - return None - - def http_response(self, request, response): - """Gets a HTTP response, if it was a GET request and the status code - starts with 2 (200 OK etc) it caches it and returns a CachedResponse - """ - if (request.get_method() == "GET" - and str(response.code).startswith("2") - ): - if 'x-local-cache' not in response.info(): - # Response is not cached - set_cache_header = store_in_cache( - self.cache_location, - request.get_full_url(), - response - ) - else: - set_cache_header = True - - return CachedResponse( - self.cache_location, - request.get_full_url(), - set_cache_header = set_cache_header - ) - else: - return response - -class CachedResponse(StringIO.StringIO): - """An urllib2.response-like object for cached responses. - - To determine if a response is cached or coming directly from - the network, check the x-local-cache header rather than the object type. - """ - - @locked_function - def __init__(self, cache_location, url, set_cache_header=True): - self.cache_location = cache_location - hpath, bpath = calculate_cache_path(cache_location, url) - - StringIO.StringIO.__init__(self, file(bpath, "rb").read()) - - self.url = url - self.code = 200 - self.msg = "OK" - headerbuf = file(hpath, "rb").read() - if set_cache_header: - headerbuf += "x-local-cache: %s\r\n" % (bpath) - self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf)) - - def info(self): - """Returns headers - """ - return self.headers - - def geturl(self): - """Returns original URL - """ - return self.url - - @locked_function - def recache(self): - new_request = urllib2.urlopen(self.url) - set_cache_header = store_in_cache( - self.cache_location, - new_request.url, - new_request - ) - CachedResponse.__init__(self, self.cache_location, self.url, True) - - @locked_function - def delete_cache(self): - delete_from_cache( - self.cache_location, - self.url - ) - - -if __name__ == "__main__": - def main(): - """Quick test/example of CacheHandler""" - opener = urllib2.build_opener(CacheHandler("/tmp/")) - response = opener.open("http://google.com") - print response.headers - print "Response:", response.read() - - response.recache() - print response.headers - print "After recache:", response.read() - - # Test usage in threads - from threading import Thread - class CacheThreadTest(Thread): - lastdata = None - def run(self): - req = opener.open("http://google.com") - newdata = req.read() - if self.lastdata is None: - self.lastdata = newdata - assert self.lastdata == newdata, "Data was not consistent, uhoh" - req.recache() - threads = [CacheThreadTest() for x in range(50)] - print "Starting threads" - [t.start() for t in threads] - print "..done" - print "Joining threads" - [t.join() for t in threads] - print "..done" - main() diff -Nru tvdb-api-1.10/tvdb_exceptions.py tvdb-api-2.0/tvdb_exceptions.py --- tvdb-api-1.10/tvdb_exceptions.py 2014-11-08 13:38:54.000000000 +0000 +++ tvdb-api-2.0/tvdb_exceptions.py 2017-09-16 10:25:07.000000000 +0000 @@ -9,44 +9,20 @@ """ __author__ = "dbr/Ben" -__version__ = "1.10" +__version__ = "2.0" -__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_shownotfound", -"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"] +import logging -class tvdb_exception(Exception): - """Any exception generated by tvdb_api - """ - pass - -class tvdb_error(tvdb_exception): - """An error with thetvdb.com (Cannot connect, for example) - """ - pass - -class tvdb_userabort(tvdb_exception): - """User aborted the interactive selection (via - the q command, ^c etc) - """ - pass - -class tvdb_shownotfound(tvdb_exception): - """Show cannot be found on thetvdb.com (non-existant show) - """ - pass - -class tvdb_seasonnotfound(tvdb_exception): - """Season cannot be found on thetvdb.com - """ - pass - -class tvdb_episodenotfound(tvdb_exception): - """Episode cannot be found on thetvdb.com - """ - pass - -class tvdb_attributenotfound(tvdb_exception): - """Raised if an episode does not have the requested - attribute (such as a episode name) - """ - pass +__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_notauthorized", "tvdb_shownotfound", +"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound", +"tvdb_resourcenotfound", "tvdb_invalidlanguage"] + +logging.getLogger(__name__).warning( + "tvdb_exceptions module is deprecated - use classes directly from tvdb_api instead") + +from tvdb_api import ( + tvdb_error, tvdb_userabort, tvdb_notauthorized, tvdb_shownotfound, + tvdb_seasonnotfound, tvdb_episodenotfound, + tvdb_resourcenotfound, tvdb_invalidlanguage, + tvdb_attributenotfound +) diff -Nru tvdb-api-1.10/tvdb_ui.py tvdb-api-2.0/tvdb_ui.py --- tvdb-api-1.10/tvdb_ui.py 2014-11-08 13:39:01.000000000 +0000 +++ tvdb-api-2.0/tvdb_ui.py 2017-09-16 10:25:16.000000000 +0000 @@ -5,45 +5,9 @@ #repository:http://github.com/dbr/tvdb_api #license:unlicense (http://unlicense.org/) -"""Contains included user interfaces for Tvdb show selection. - -A UI is a callback. A class, it's __init__ function takes two arguments: - -- config, which is the Tvdb config dict, setup in tvdb_api.py -- log, which is Tvdb's logger instance (which uses the logging module). You can -call log.info() log.warning() etc - -It must have a method "selectSeries", this is passed a list of dicts, each dict -contains the the keys "name" (human readable show name), and "sid" (the shows -ID as on thetvdb.com). For example: - -[{'name': u'Lost', 'sid': u'73739'}, - {'name': u'Lost Universe', 'sid': u'73181'}] - -The "selectSeries" method must return the appropriate dict, or it can raise -tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show -cannot be found). - -A simple example callback, which returns a random series: - ->>> import random ->>> from tvdb_ui import BaseUI ->>> class RandomUI(BaseUI): -... def selectSeries(self, allSeries): -... import random -... return random.choice(allSeries) - -Then to use it.. - ->>> from tvdb_api import Tvdb ->>> t = Tvdb(custom_ui = RandomUI) ->>> random_matching_series = t['Lost'] ->>> type(random_matching_series) - -""" __author__ = "dbr/Ben" -__version__ = "1.10" +__version__ = "2.0" import sys import logging @@ -51,117 +15,7 @@ from tvdb_exceptions import tvdb_userabort +logging.getLogger(__name__).warning( + "tvdb_ui module is deprecated - use classes directly from tvdb_api instead") -IS_PY2 = sys.version_info[0] == 2 - -if IS_PY2: - user_input = raw_input -else: - user_input = input - - -def log(): - return logging.getLogger(__name__) - -class BaseUI: - """Default non-interactive UI, which auto-selects first results - """ - def __init__(self, config, log = None): - self.config = config - if log is not None: - warnings.warn("the UI's log parameter is deprecated, instead use\n" - "use import logging; logging.getLogger('ui').info('blah')\n" - "The self.log attribute will be removed in the next version") - self.log = logging.getLogger(__name__) - - def selectSeries(self, allSeries): - return allSeries[0] - - -class ConsoleUI(BaseUI): - """Interactively allows the user to select a show from a console based UI - """ - - def _displaySeries(self, allSeries, limit = 6): - """Helper function, lists series with corresponding ID - """ - if limit is not None: - toshow = allSeries[:limit] - else: - toshow = allSeries - - print("TVDB Search Results:") - for i, cshow in enumerate(toshow): - i_show = i + 1 # Start at more human readable number 1 (not 0) - log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesname'])) - if i == 0: - extra = " (default)" - else: - extra = "" - - output = "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % ( - i_show, - cshow['seriesname'], - cshow['language'], - str(cshow['id']), - cshow['lid'], - extra - ) - if IS_PY2: - print(output.encode("UTF-8", "ignore")) - else: - print(output) - - def selectSeries(self, allSeries): - self._displaySeries(allSeries) - - if len(allSeries) == 1: - # Single result, return it! - print("Automatically selecting only result") - return allSeries[0] - - if self.config['select_first'] is True: - print("Automatically returning first search result") - return allSeries[0] - - while True: # return breaks this loop - try: - print("Enter choice (first number, return for default, 'all', ? for help):") - ans = user_input() - except KeyboardInterrupt: - raise tvdb_userabort("User aborted (^c keyboard interupt)") - except EOFError: - raise tvdb_userabort("User aborted (EOF received)") - - log().debug('Got choice of: %s' % (ans)) - try: - selected_id = int(ans) - 1 # The human entered 1 as first result, not zero - except ValueError: # Input was not number - if len(ans.strip()) == 0: - # Default option - log().debug('Default option, returning first series') - return allSeries[0] - if ans == "q": - log().debug('Got quit command (q)') - raise tvdb_userabort("User aborted ('q' quit command)") - elif ans == "?": - print("## Help") - print("# Enter the number that corresponds to the correct show.") - print("# a - display all results") - print("# all - display all results") - print("# ? - this help") - print("# q - abort tvnamer") - print("# Press return with no input to select first result") - elif ans.lower() in ["a", "all"]: - self._displaySeries(allSeries, limit = None) - else: - log().debug('Unknown keypress %s' % (ans)) - else: - log().debug('Trying to return ID: %d' % (selected_id)) - try: - return allSeries[selected_id] - except IndexError: - log().debug('Invalid show number entered!') - print("Invalid number (%s) selected!") - self._displaySeries(allSeries) - +from tvdb_api import BaseUI, ConsoleUI