Overview

Request 4327 (accepted)

- Update to 2.0:
* Switch to TheTVDB new JSON based API - issue #57
- Add support for Python3
- Fix: non-executable-script
- Update to description
- Spec file cleanup

Submit package home:awissu...:Multimedia / python-tvdb_api to package Multimedia / python-tvdb_api

python-tvdb_api.changes Changed
x
 
1
@@ -1,4 +1,14 @@
2
 -------------------------------------------------------------------
3
+Thu Nov  1 16:49:30 UTC 2018 - Aliaksei Padvalski <avvissu@yandex.by>
4
+
5
+- Update to 2.0:
6
+  * Switch to TheTVDB new JSON based API - issue #57
7
+- Add support for Python3
8
+- Fix: non-executable-script
9
+- Update to description
10
+- Spec file cleanup
11
+
12
+-------------------------------------------------------------------
13
 Fri Apr 21 12:40:16 UTC 2017 - aloisio@gmx.com
14
 
15
 - Update to version 1.10
16
python-tvdb_api.spec Changed
91
 
1
@@ -1,6 +1,7 @@
2
 #
3
 # spec file for package python-tvdb_api
4
 #
5
+# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
6
 # Copyright (c) 2017 Packman Team <packman@links2linux.de>
7
 #
8
 # All modifications and additions to the file contributed by third parties
9
@@ -16,42 +17,60 @@
10
 #
11
 
12
 
13
-%define modname tvdb_api
14
+%{?!python_module:%define python_module() %{!?skip_python2:python-%{**}} %{!?skip_python3:python3-%{**}}}
15
 Name:           python-tvdb_api
16
-Version:        1.10
17
+Version:        2.0
18
 Release:        0
19
-Summary:        Python module to access the API from thetvdb.com
20
+Summary:        Interface to thetvdb.com
21
 # The UnLicense (https://unlicense.org)
22
 License:        SUSE-Permissive
23
 Group:          Productivity/Multimedia/Other
24
-Url:            https://github.com/dbr/tvdb_api/tree/master
25
-Source0:        https://files.pythonhosted.org/packages/source/t/%{modname}/%{modname}-%{version}.tar.gz
26
-BuildRequires:  python-devel
27
-BuildRequires:  python-setuptools
28
-BuildRoot:      %{_tmppath}/%{name}-%{version}-build
29
+URL:            https://github.com/dbr/tvdb_api
30
+Source0:        https://files.pythonhosted.org/packages/source/t/tvdb_api/tvdb_api-%{version}.tar.gz
31
+BuildRequires:  %{python_module devel}
32
+BuildRequires:  %{python_module rpm-macros}
33
+BuildRequires:  %{python_module setuptools}
34
+BuildRequires:  fdupes
35
+Requires:       python-requests-cache
36
 BuildArch:      noarch
37
-%py_requires
38
+%python_subpackages
39
 
40
 %description
41
-tvdb_api is an easy to use interface to thetvdb.com via python
42
+An easy to use API interface to TheTVDB.com.
43
 
44
 %prep
45
-%setup -q -n %{modname}-%{version}
46
-for file in {tvdb_cache,tvdb_api,tvdb_ui,tvdb_exceptions}
47
-do
48
-    sed -i "1d" $file.py
49
-done
50
+%setup -q -n tvdb_api-%{version}
51
 
52
 %build
53
-python setup.py build
54
+%python_build
55
 
56
 %install
57
-python setup.py install --root=%{buildroot} --prefix=%{_prefix}
58
+%python_install
59
+
60
+# Fix: non-executable-script
61
+%{python_expand \
62
+pushd %{buildroot}%{$python_sitelib}
63
+for _file in $(grep -rl '^\#\!\/'); do
64
+  find -name ${_file##*/} -type f -not -perm /111 -exec sed '/^\#\!\//d' -i {} +
65
+  find -name ${_file##*/} -type f -perm /111 -exec sed 's|#!%{_bindir}/env python|#!%__$python|' -i {} +
66
+done
67
+popd
68
+}
69
+
70
+%if 0%{?have_python2}
71
+%py_compile %{buildroot}%{python2_sitelib}
72
+%py_compile -O %{buildroot}%{python2_sitelib}
73
+%endif
74
+%if 0%{?have_python3}
75
+%py3_compile %{buildroot}%{python3_sitelib}
76
+%py3_compile -O %{buildroot}%{python3_sitelib}
77
+%endif
78
+#
79
+
80
+%python_expand %fdupes %{buildroot}%{$python_sitelib}
81
 
82
-%files
83
-%defattr(-,root,root)
84
+%files %{python_files}
85
 %doc UNLICENSE readme.md
86
-%{python_sitelib}/tvdb_*.py*
87
-%{python_sitelib}/%{modname}-%{version}-py%{python_version}.egg-info
88
+%{python_sitelib}/*
89
 
90
 %changelog
91
tvdb_api-1.10.tar.gz/tvdb_cache.py Deleted
253
 
1
@@ -1,251 +0,0 @@
2
-#!/usr/bin/env python
3
-#encoding:utf-8
4
-#author:dbr/Ben
5
-#project:tvdb_api
6
-#repository:http://github.com/dbr/tvdb_api
7
-#license:unlicense (http://unlicense.org/)
8
-
9
-"""
10
-urllib2 caching handler
11
-Modified from http://code.activestate.com/recipes/491261/
12
-"""
13
-from __future__ import with_statement
14
-
15
-__author__ = "dbr/Ben"
16
-__version__ = "1.10"
17
-
18
-import os
19
-import time
20
-import errno
21
-import httplib
22
-import urllib2
23
-import StringIO
24
-from hashlib import md5
25
-from threading import RLock
26
-
27
-cache_lock = RLock()
28
-
29
-def locked_function(origfunc):
30
-    """Decorator to execute function under lock"""
31
-    def wrapped(*args, **kwargs):
32
-        cache_lock.acquire()
33
-        try:
34
-            return origfunc(*args, **kwargs)
35
-        finally:
36
-            cache_lock.release()
37
-    return wrapped
38
-
39
-def calculate_cache_path(cache_location, url):
40
-    """Checks if [cache_location]/[hash_of_url].headers and .body exist
41
-    """
42
-    thumb = md5(url).hexdigest()
43
-    header = os.path.join(cache_location, thumb + ".headers")
44
-    body = os.path.join(cache_location, thumb + ".body")
45
-    return header, body
46
-
47
-def check_cache_time(path, max_age):
48
-    """Checks if a file has been created/modified in the [last max_age] seconds.
49
-    False means the file is too old (or doesn't exist), True means it is
50
-    up-to-date and valid"""
51
-    if not os.path.isfile(path):
52
-        return False
53
-    cache_modified_time = os.stat(path).st_mtime
54
-    time_now = time.time()
55
-    if cache_modified_time < time_now - max_age:
56
-        # Cache is old
57
-        return False
58
-    else:
59
-        return True
60
-
61
-@locked_function
62
-def exists_in_cache(cache_location, url, max_age):
63
-    """Returns if header AND body cache file exist (and are up-to-date)"""
64
-    hpath, bpath = calculate_cache_path(cache_location, url)
65
-    if os.path.exists(hpath) and os.path.exists(bpath):
66
-        return(
67
-            check_cache_time(hpath, max_age)
68
-            and check_cache_time(bpath, max_age)
69
-        )
70
-    else:
71
-        # File does not exist
72
-        return False
73
-
74
-@locked_function
75
-def store_in_cache(cache_location, url, response):
76
-    """Tries to store response in cache."""
77
-    hpath, bpath = calculate_cache_path(cache_location, url)
78
-    try:
79
-        outf = open(hpath, "wb")
80
-        headers = str(response.info())
81
-        outf.write(headers)
82
-        outf.close()
83
-
84
-        outf = open(bpath, "wb")
85
-        outf.write(response.read())
86
-        outf.close()
87
-    except IOError:
88
-        return True
89
-    else:
90
-        return False
91
-        
92
-@locked_function
93
-def delete_from_cache(cache_location, url):
94
-    """Deletes a response in cache."""
95
-    hpath, bpath = calculate_cache_path(cache_location, url)
96
-    try:
97
-        if os.path.exists(hpath):
98
-            os.remove(hpath)
99
-        if os.path.exists(bpath):
100
-            os.remove(bpath)
101
-    except IOError:
102
-        return True
103
-    else:
104
-        return False
105
-
106
-class CacheHandler(urllib2.BaseHandler):
107
-    """Stores responses in a persistant on-disk cache.
108
-
109
-    If a subsequent GET request is made for the same URL, the stored
110
-    response is returned, saving time, resources and bandwidth
111
-    """
112
-    @locked_function
113
-    def __init__(self, cache_location, max_age = 21600):
114
-        """The location of the cache directory"""
115
-        self.max_age = max_age
116
-        self.cache_location = cache_location
117
-        if not os.path.exists(self.cache_location):
118
-            try:
119
-                os.mkdir(self.cache_location)
120
-            except OSError, e:
121
-                if e.errno == errno.EEXIST and os.path.isdir(self.cache_location):
122
-                    # File exists, and it's a directory,
123
-                    # another process beat us to creating this dir, that's OK.
124
-                    pass
125
-                else:
126
-                    # Our target dir is already a file, or different error,
127
-                    # relay the error!
128
-                    raise
129
-
130
-    def default_open(self, request):
131
-        """Handles GET requests, if the response is cached it returns it
132
-        """
133
-        if request.get_method() is not "GET":
134
-            return None # let the next handler try to handle the request
135
-
136
-        if exists_in_cache(
137
-            self.cache_location, request.get_full_url(), self.max_age
138
-        ):
139
-            return CachedResponse(
140
-                self.cache_location,
141
-                request.get_full_url(),
142
-                set_cache_header = True
143
-            )
144
-        else:
145
-            return None
146
-
147
-    def http_response(self, request, response):
148
-        """Gets a HTTP response, if it was a GET request and the status code
149
-        starts with 2 (200 OK etc) it caches it and returns a CachedResponse
150
-        """
151
-        if (request.get_method() == "GET"
152
-            and str(response.code).startswith("2")
153
-        ):
154
-            if 'x-local-cache' not in response.info():
155
-                # Response is not cached
156
-                set_cache_header = store_in_cache(
157
-                    self.cache_location,
158
-                    request.get_full_url(),
159
-                    response
160
-                )
161
-            else:
162
-                set_cache_header = True
163
-
164
-            return CachedResponse(
165
-                self.cache_location,
166
-                request.get_full_url(),
167
-                set_cache_header = set_cache_header
168
-            )
169
-        else:
170
-            return response
171
-
172
-class CachedResponse(StringIO.StringIO):
173
-    """An urllib2.response-like object for cached responses.
174
-
175
-    To determine if a response is cached or coming directly from
176
-    the network, check the x-local-cache header rather than the object type.
177
-    """
178
-
179
-    @locked_function
180
-    def __init__(self, cache_location, url, set_cache_header=True):
181
-        self.cache_location = cache_location
182
-        hpath, bpath = calculate_cache_path(cache_location, url)
183
-
184
-        StringIO.StringIO.__init__(self, file(bpath, "rb").read())
185
-
186
-        self.url     = url
187
-        self.code    = 200
188
-        self.msg     = "OK"
189
-        headerbuf = file(hpath, "rb").read()
190
-        if set_cache_header:
191
-            headerbuf += "x-local-cache: %s\r\n" % (bpath)
192
-        self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf))
193
-
194
-    def info(self):
195
-        """Returns headers
196
-        """
197
-        return self.headers
198
-
199
-    def geturl(self):
200
-        """Returns original URL
201
-        """
202
-        return self.url
203
-
204
-    @locked_function
205
-    def recache(self):
206
-        new_request = urllib2.urlopen(self.url)
207
-        set_cache_header = store_in_cache(
208
-            self.cache_location,
209
-            new_request.url,
210
-            new_request
211
-        )
212
-        CachedResponse.__init__(self, self.cache_location, self.url, True)
213
-
214
-    @locked_function
215
-    def delete_cache(self):
216
-        delete_from_cache(
217
-            self.cache_location,
218
-            self.url
219
-        )
220
-    
221
-
222
-if __name__ == "__main__":
223
-    def main():
224
-        """Quick test/example of CacheHandler"""
225
-        opener = urllib2.build_opener(CacheHandler("/tmp/"))
226
-        response = opener.open("http://google.com")
227
-        print response.headers
228
-        print "Response:", response.read()
229
-
230
-        response.recache()
231
-        print response.headers
232
-        print "After recache:", response.read()
233
-
234
-        # Test usage in threads
235
-        from threading import Thread
236
-        class CacheThreadTest(Thread):
237
-            lastdata = None
238
-            def run(self):
239
-                req = opener.open("http://google.com")
240
-                newdata = req.read()
241
-                if self.lastdata is None:
242
-                    self.lastdata = newdata
243
-                assert self.lastdata == newdata, "Data was not consistent, uhoh"
244
-                req.recache()
245
-        threads = [CacheThreadTest() for x in range(50)]
246
-        print "Starting threads"
247
-        [t.start() for t in threads]
248
-        print "..done"
249
-        print "Joining threads"
250
-        [t.join() for t in threads]
251
-        print "..done"
252
-    main()
253
tvdb_api-1.10.tar.gz/PKG-INFO -> tvdb_api-2.0.tar.gz/PKG-INFO Changed
27
 
1
@@ -1,6 +1,6 @@
2
 Metadata-Version: 1.1
3
 Name: tvdb_api
4
-Version: 1.10
5
+Version: 2.0
6
 Summary: Interface to thetvdb.com
7
 Home-page: http://github.com/dbr/tvdb_api/tree/master
8
 Author: dbr/Ben
9
@@ -14,7 +14,7 @@
10
         >>> ep = t['My Name Is Earl'][1][22]
11
         >>> ep
12
         <Episode 01x22 - Stole a Badge>
13
-        >>> ep['episodename']
14
+        >>> ep['episodeName']
15
         u'Stole a Badge'
16
         
17
 Platform: UNKNOWN
18
@@ -27,6 +27,8 @@
19
 Classifier: Programming Language :: Python :: 2.7
20
 Classifier: Programming Language :: Python :: 3.3
21
 Classifier: Programming Language :: Python :: 3.4
22
+Classifier: Programming Language :: Python :: 3.5
23
+Classifier: Programming Language :: Python :: 3.6
24
 Classifier: Topic :: Multimedia
25
 Classifier: Topic :: Utilities
26
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
tvdb_api-1.10.tar.gz/Rakefile -> tvdb_api-2.0.tar.gz/Rakefile Changed
10
 
1
@@ -47,7 +47,7 @@
2
 end
3
 
4
 desc "Upload current version to PyPi"
5
-task :topypi do
6
+task :topypi => :test do
7
   cur_file = File.open("tvdb_api.py").read()
8
   tvdb_api_version = cur_file.scan(/__version__ = "(.*)"/)
9
   tvdb_api_version = tvdb_api_version[0][0]
10
tvdb_api-1.10.tar.gz/readme.md -> tvdb_api-2.0.tar.gz/readme.md Changed
19
 
1
@@ -65,7 +65,7 @@
2
 The data is stored in an attribute named `data`, within the Show instance:
3
 
4
     >>> t['scrubs'].data.keys()
5
-    ['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']
6
+    ['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']
7
 
8
 Although each element is also accessible via `t['scrubs']` for ease-of-use:
9
 
10
@@ -105,7 +105,7 @@
11
 Remember a simple list of actors is accessible via the default Show data:
12
 
13
     >>> t['scrubs']['actors']
14
-    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|'
15
+    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|'
16
 
17
 [tvdb]: http://thetvdb.com
18
 [tvnamer]: http://github.com/dbr/tvnamer
19
tvdb_api-1.10.tar.gz/setup.py -> tvdb_api-2.0.tar.gz/setup.py Changed
73
 
1
@@ -1,25 +1,34 @@
2
 import sys
3
 from setuptools import setup
4
+from setuptools.command.test import test as TestCommand
5
 
6
-IS_PY2 = sys.version_info[0] == 2
7
 
8
-_requirements = []
9
-if not IS_PY2:
10
-    _requirements.append('requests_cache')
11
+class PyTest(TestCommand):
12
+    user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
13
 
14
-    # 'requests' is installed as requirement by requests-cache,
15
-    # commented out because it triggers a bug in setuptool:
16
-    # https://bitbucket.org/pypa/setuptools/issue/196/tests_require-pytest-pytest-cov-breaks
17
+    def initialize_options(self):
18
+        TestCommand.initialize_options(self)
19
+        self.pytest_args = []
20
 
21
+    def finalize_options(self):
22
+        TestCommand.finalize_options(self)
23
+        self.test_args = []
24
+        self.test_suite = True
25
 
26
+    def run_tests(self):
27
+        #import here, cause outside the eggs aren't loaded
28
+        import pytest
29
+        errno = pytest.main(self.pytest_args)
30
+        sys.exit(errno)
31
+
32
+
33
+_requirements = ['requests_cache', 'requests']
34
 _modules = ['tvdb_api', 'tvdb_ui', 'tvdb_exceptions']
35
-if IS_PY2:
36
-    _modules.append('tvdb_cache')
37
 
38
 
39
 setup(
40
 name = 'tvdb_api',
41
-version='1.10',
42
+version='2.0',
43
 
44
 author='dbr/Ben',
45
 description='Interface to thetvdb.com',
46
@@ -35,13 +44,16 @@
47
 >>> ep = t['My Name Is Earl'][1][22]
48
 >>> ep
49
 <Episode 01x22 - Stole a Badge>
50
->>> ep['episodename']
51
+>>> ep['episodeName']
52
 u'Stole a Badge'
53
 """,
54
 
55
 py_modules = _modules,
56
 install_requires = _requirements,
57
 
58
+tests_require=['pytest'],
59
+cmdclass = {'test': PyTest},
60
+
61
 classifiers=[
62
     "Intended Audience :: Developers",
63
     "Natural Language :: English",
64
@@ -52,6 +64,8 @@
65
     "Programming Language :: Python :: 2.7",
66
     "Programming Language :: Python :: 3.3",
67
     "Programming Language :: Python :: 3.4",
68
+    "Programming Language :: Python :: 3.5",
69
+    "Programming Language :: Python :: 3.6",
70
     "Topic :: Multimedia",
71
     "Topic :: Utilities",
72
     "Topic :: Software Development :: Libraries :: Python Modules",
73
tvdb_api-1.10.tar.gz/tests/test_tvdb_api.py -> tvdb_api-2.0.tar.gz/tests/test_tvdb_api.py Changed
760
 
1
@@ -11,75 +11,58 @@
2
 import os
3
 import sys
4
 import datetime
5
-import unittest
6
+import pytest
7
 
8
 # Force parent directory onto path
9
 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10
 
11
 import tvdb_api
12
-import tvdb_ui
13
 from tvdb_api import (tvdb_shownotfound, tvdb_seasonnotfound,
14
-tvdb_episodenotfound, tvdb_attributenotfound)
15
+                      tvdb_episodenotfound, tvdb_attributenotfound)
16
 
17
 
18
-IS_PY2 = sys.version_info[0] == 2
19
-
20
-
21
-class test_tvdb_basic(unittest.TestCase):
22
+class TestTvdbBasic:
23
     # Used to store the cached instance of Tvdb()
24
     t = None
25
-    
26
-    def setUp(self):
27
-        if self.t is None:
28
-            self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
29
-     
30
+
31
+    @classmethod
32
+    def setup_class(cls):
33
+        if cls.t is None:
34
+            cls.t = tvdb_api.Tvdb(cache=True, banners=False)
35
+
36
     def test_different_case(self):
37
         """Checks the auto-correction of show names is working.
38
         It should correct the weirdly capitalised 'sCruBs' to 'Scrubs'
39
         """
40
-        self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady')
41
-        self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs')
42
+        assert self.t['scrubs'][1][4]['episodeName'] == 'My Old Lady'
43
+        assert self.t['sCruBs']['seriesName'] == 'Scrubs'
44
 
45
     def test_spaces(self):
46
         """Checks shownames with spaces
47
         """
48
-        self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl')
49
-        self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death')
50
+        assert self.t['My Name Is Earl']['seriesName'] == 'My Name Is Earl'
51
+        assert self.t['My Name Is Earl'][1][4]['episodeName'] == 'Faked My Own Death'
52
 
53
     def test_numeric(self):
54
         """Checks numeric show names
55
         """
56
-        self.assertEquals(self.t['24'][2][20]['episodename'], 'Day 2: 3:00 A.M.-4:00 A.M.')
57
-        self.assertEquals(self.t['24']['seriesname'], '24')
58
+        assert self.t['24'][2][20]['episodeName'] == 'Day 2: 3:00 A.M. - 4:00 A.M.'
59
+        assert self.t['24']['seriesName'] == '24'
60
 
61
     def test_show_iter(self):
62
         """Iterating over a show returns each seasons
63
         """
64
-        self.assertEquals(
65
-            len(
66
-                [season for season in self.t['Life on Mars']]
67
-            ),
68
-            2
69
-        )
70
-    
71
+        assert len([season for season in self.t['Life on Mars']]) == 2
72
+
73
     def test_season_iter(self):
74
         """Iterating over a show returns episodes
75
         """
76
-        self.assertEquals(
77
-            len(
78
-                [episode for episode in self.t['Life on Mars'][1]]
79
-            ),
80
-            8
81
-        )
82
+        assert len([episode for episode in self.t['Life on Mars'][1]]) == 8
83
 
84
     def test_get_episode_overview(self):
85
         """Checks episode overview is retrieved correctly.
86
         """
87
-        self.assertEquals(
88
-            self.t['Battlestar Galactica (2003)'][1][6]['overview'].startswith(
89
-                'When a new copy of Doral, a Cylon who had been previously'),
90
-            True
91
-        )
92
+        assert self.t['Battlestar Galactica (2003)'][1][6]['overview'].startswith('When a new copy of Doral, a Cylon who had been previously') == True
93
 
94
     def test_get_parent(self):
95
         """Check accessing series from episode instance
96
@@ -88,329 +71,251 @@
97
         season = show[1]
98
         episode = show[1][1]
99
 
100
-        self.assertEquals(
101
-            season.show,
102
-            show
103
-        )
104
-
105
-        self.assertEquals(
106
-            episode.season,
107
-            season
108
-        )
109
-
110
-        self.assertEquals(
111
-            episode.season.show,
112
-            show
113
-        )
114
+        assert season.show == show
115
+        assert episode.season == season
116
+        assert episode.season.show == show
117
 
118
     def test_no_season(self):
119
         show = self.t['Katekyo Hitman Reborn']
120
         print(tvdb_api)
121
         print(show[1][1])
122
 
123
-class test_tvdb_errors(unittest.TestCase):
124
-    # Used to store the cached instance of Tvdb()
125
+
126
+class TestTvdbErrors:
127
     t = None
128
-    
129
-    def setUp(self):
130
-        if self.t is None:
131
-            self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
132
+
133
+    @classmethod
134
+    def setup_class(cls):
135
+        if cls.t is None:
136
+            cls.t = tvdb_api.Tvdb(cache=True, banners=False)
137
 
138
     def test_seasonnotfound(self):
139
         """Checks exception is thrown when season doesn't exist.
140
         """
141
-        self.assertRaises(tvdb_seasonnotfound, lambda:self.t['CNNNN'][10][1])
142
+        with pytest.raises(tvdb_seasonnotfound):
143
+            self.t['CNNNN'][10]
144
 
145
     def test_shownotfound(self):
146
         """Checks exception is thrown when episode doesn't exist.
147
         """
148
-        self.assertRaises(tvdb_shownotfound, lambda:self.t['the fake show thingy'])
149
-    
150
+        with pytest.raises(tvdb_shownotfound):
151
+            self.t['the fake show thingy']
152
+
153
     def test_episodenotfound(self):
154
         """Checks exception is raised for non-existent episode
155
         """
156
-        self.assertRaises(tvdb_episodenotfound, lambda:self.t['Scrubs'][1][30])
157
+        with pytest.raises(tvdb_episodenotfound):
158
+            self.t['Scrubs'][1][30]
159
 
160
     def test_attributenamenotfound(self):
161
         """Checks exception is thrown for if an attribute isn't found.
162
         """
163
-        self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN'][1][6]['afakeattributething'])
164
-        self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN']['afakeattributething'])
165
+        with pytest.raises(tvdb_attributenotfound):
166
+            self.t['CNNNN'][1][6]['afakeattributething']
167
+            self.t['CNNNN']['afakeattributething']
168
 
169
-class test_tvdb_search(unittest.TestCase):
170
+
171
+class TestTvdbSearch:
172
     # Used to store the cached instance of Tvdb()
173
     t = None
174
-    
175
-    def setUp(self):
176
-        if self.t is None:
177
-            self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
178
+
179
+    @classmethod
180
+    def setup_class(cls):
181
+        if cls.t is None:
182
+            cls.t = tvdb_api.Tvdb(cache=True, banners=False)
183
 
184
     def test_search_len(self):
185
         """There should be only one result matching
186
         """
187
-        self.assertEquals(len(self.t['My Name Is Earl'].search('Faked His Own Death')), 1)
188
+        assert len(self.t['My Name Is Earl'].search('Faked My Own Death')) == 1
189
 
190
     def test_search_checkname(self):
191
         """Checks you can get the episode name of a search result
192
         """
193
-        self.assertEquals(self.t['Scrubs'].search('my first')[0]['episodename'], 'My First Day')
194
-        self.assertEquals(self.t['My Name Is Earl'].search('Faked His Own Death')[0]['episodename'], 'Faked His Own Death')
195
-    
196
+        assert self.t['Scrubs'].search('my first')[0]['episodeName'] == 'My First Day'
197
+        assert self.t['My Name Is Earl'].search('Faked My Own Death')[0]['episodeName'] == 'Faked My Own Death'
198
+
199
     def test_search_multiresults(self):
200
         """Checks search can return multiple results
201
         """
202
-        self.assertEquals(len(self.t['Scrubs'].search('my first')) >= 3, True)
203
+        assert (len(self.t['Scrubs'].search('my first')) >= 3) == True
204
 
205
     def test_search_no_params_error(self):
206
         """Checks not supplying search info raises TypeError"""
207
-        self.assertRaises(
208
-            TypeError,
209
-            lambda: self.t['Scrubs'].search()
210
-        )
211
+        with pytest.raises(TypeError):
212
+            self.t['Scrubs'].search()
213
 
214
     def test_search_season(self):
215
         """Checks the searching of a single season"""
216
-        self.assertEquals(
217
-            len(self.t['Scrubs'][1].search("First")),
218
-            3
219
-        )
220
-    
221
+        assert len(self.t['Scrubs'][1].search("First")) == 3
222
+
223
     def test_search_show(self):
224
         """Checks the searching of an entire show"""
225
-        self.assertEquals(
226
-            len(self.t['CNNNN'].search('CNNNN', key='episodename')),
227
-            3
228
-        )
229
+        assert len(self.t['CNNNN'].search('CNNNN', key='episodeName')) == 3
230
 
231
     def test_aired_on(self):
232
         """Tests airedOn show method"""
233
         sr = self.t['Scrubs'].airedOn(datetime.date(2001, 10, 2))
234
-        self.assertEquals(len(sr), 1)
235
-        self.assertEquals(sr[0]['episodename'], u'My First Day')
236
+        assert len(sr) == 1
237
+        assert sr[0]['episodeName'] == u'My First Day'
238
 
239
-class test_tvdb_data(unittest.TestCase):
240
+
241
+class TestTvdbData:
242
     # Used to store the cached instance of Tvdb()
243
     t = None
244
-    
245
-    def setUp(self):
246
-        if self.t is None:
247
-            self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
248
+
249
+    @classmethod
250
+    def setup_class(cls):
251
+        if cls.t is None:
252
+            cls.t = tvdb_api.Tvdb(cache=True, banners=False)
253
 
254
     def test_episode_data(self):
255
         """Check the firstaired value is retrieved
256
         """
257
-        self.assertEquals(
258
-            self.t['lost']['firstaired'],
259
-            '2004-09-22'
260
-        )
261
+        assert self.t['lost']['firstAired'] == '2004-09-22'
262
 
263
-class test_tvdb_misc(unittest.TestCase):
264
+
265
+class TestTvdbMisc:
266
     # Used to store the cached instance of Tvdb()
267
     t = None
268
-    
269
-    def setUp(self):
270
-        if self.t is None:
271
-            self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
272
+
273
+    @classmethod
274
+    def setup_class(cls):
275
+        if cls.t is None:
276
+            cls.t = tvdb_api.Tvdb(cache=True, banners=False)
277
 
278
     def test_repr_show(self):
279
         """Check repr() of Season
280
         """
281
-        self.assertEquals(
282
-            repr(self.t['CNNNN']),
283
-            "<Show Chaser Non-Stop News Network (CNNNN) (containing 3 seasons)>"
284
-        )
285
+        assert repr(self.t['CNNNN']).replace("u'", "'") == "<Show 'Chaser Non-Stop News Network (CNNNN)' (containing 3 seasons)>"
286
+
287
     def test_repr_season(self):
288
         """Check repr() of Season
289
         """
290
-        self.assertEquals(
291
-            repr(self.t['CNNNN'][1]),
292
-            "<Season instance (containing 9 episodes)>"
293
-        )
294
+        assert repr(self.t['CNNNN'][1]) == "<Season instance (containing 9 episodes)>"
295
+
296
     def test_repr_episode(self):
297
         """Check repr() of Episode
298
         """
299
-        self.assertEquals(
300
-            repr(self.t['CNNNN'][1][1]),
301
-            "<Episode 01x01 - Terror Alert>"
302
-        )
303
+        assert repr(self.t['CNNNN'][1][1]).replace("u'", "'") == "<Episode 01x01 - 'Terror Alert'>"
304
+
305
     def test_have_all_languages(self):
306
         """Check valid_languages is up-to-date (compared to languages.xml)
307
         """
308
-        et = self.t._getetsrc(
309
-            "http://thetvdb.com/api/%s/languages.xml" % (
310
-                self.t.config['apikey']
311
-            )
312
-        )
313
-        languages = [x.find("abbreviation").text for x in et.findall("Language")]
314
-        
315
-        self.assertEquals(
316
-            sorted(languages),
317
-            sorted(self.t.config['valid_languages'])
318
-        )
319
-        
320
-class test_tvdb_languages(unittest.TestCase):
321
+        et = self.t._getetsrc("https://api.thetvdb.com/languages")
322
+
323
+        languages = [x['abbreviation'] for x in et]
324
+
325
+        assert sorted(languages) == sorted(self.t.config['valid_languages'])
326
+
327
+
328
+class TestTvdbLanguages:
329
     def test_episode_name_french(self):
330
         """Check episode data is in French (language="fr")
331
         """
332
         t = tvdb_api.Tvdb(cache = True, language = "fr")
333
-        self.assertEquals(
334
-            t['scrubs'][1][1]['episodename'],
335
-            "Mon premier jour"
336
-        )
337
-        self.assertTrue(
338
-            t['scrubs']['overview'].startswith(
339
-                u"J.D. est un jeune m\xe9decin qui d\xe9bute"
340
-            )
341
-        )
342
+        assert t['scrubs'][1][1]['episodeName'] == "Mon premier jour"
343
+        assert t['scrubs']['overview'].startswith(u"J.D. est un jeune m\xe9decin qui d\xe9bute")
344
 
345
     def test_episode_name_spanish(self):
346
         """Check episode data is in Spanish (language="es")
347
         """
348
         t = tvdb_api.Tvdb(cache = True, language = "es")
349
-        self.assertEquals(
350
-            t['scrubs'][1][1]['episodename'],
351
-            "Mi Primer Dia"
352
-        )
353
-        self.assertTrue(
354
-            t['scrubs']['overview'].startswith(
355
-                u'Scrubs es una divertida comedia'
356
-            )
357
-        )
358
+        assert t['scrubs'][1][1]['episodeName'] == u'Mi primer día'
359
+        assert t['scrubs']['overview'].startswith(u'Scrubs es una divertida comedia')
360
 
361
     def test_multilanguage_selection(self):
362
         """Check selected language is used
363
         """
364
-        class SelectEnglishUI(tvdb_ui.BaseUI):
365
-            def selectSeries(self, allSeries):
366
-                return [x for x in allSeries if x['language'] == "en"][0]
367
-
368
-        class SelectItalianUI(tvdb_ui.BaseUI):
369
-            def selectSeries(self, allSeries):
370
-                return [x for x in allSeries if x['language'] == "it"][0]
371
-
372
         t_en = tvdb_api.Tvdb(
373
             cache=True,
374
-            custom_ui = SelectEnglishUI,
375
             language = "en")
376
         t_it = tvdb_api.Tvdb(
377
             cache=True,
378
-            custom_ui = SelectItalianUI,
379
             language = "it")
380
 
381
-        self.assertEquals(
382
-            t_en['dexter'][1][2]['episodename'], "Crocodile"
383
-        )
384
-        self.assertEquals(
385
-            t_it['dexter'][1][2]['episodename'], "Lacrime di coccodrillo"
386
-        )
387
+        assert t_en['dexter'][1][2]['episodeName'] == "Crocodile"
388
+        assert t_it['dexter'][1][2]['episodeName'] == "Lacrime di coccodrillo"
389
 
390
 
391
-class test_tvdb_unicode(unittest.TestCase):
392
+class TestTvdbUnicode:
393
     def test_search_in_chinese(self):
394
         """Check searching for show with language=zh returns Chinese seriesname
395
         """
396
-        t = tvdb_api.Tvdb(cache = True, language = "zh")
397
+        t = tvdb_api.Tvdb(cache=True, language="zh")
398
         show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i']
399
-        self.assertEquals(
400
-            type(show),
401
-            tvdb_api.Show
402
-        )
403
-        
404
-        self.assertEquals(
405
-            show['seriesname'],
406
-            u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'
407
-        )
408
+        assert type(show) == tvdb_api.Show
409
+        assert show['seriesName'] == u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'
410
 
411
+    @pytest.mark.skip('Новое API не возвращает сразу все языки')
412
     def test_search_in_all_languages(self):
413
         """Check search_all_languages returns Chinese show, with language=en
414
         """
415
-        t = tvdb_api.Tvdb(cache = True, search_all_languages = True, language="en")
416
+        t = tvdb_api.Tvdb(cache=True, search_all_languages=True, language="en")
417
         show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i']
418
-        self.assertEquals(
419
-            type(show),
420
-            tvdb_api.Show
421
-        )
422
-        
423
-        self.assertEquals(
424
-            show['seriesname'],
425
-            u'Virtues Of Harmony II'
426
-        )
427
-
428
-class test_tvdb_banners(unittest.TestCase):
429
+        assert type(show) == tvdb_api.Show
430
+        assert show['seriesName'] == u'Virtues Of Harmony II'
431
+
432
+
433
+class TestTvdbBanners:
434
     # Used to store the cached instance of Tvdb()
435
     t = None
436
-    
437
-    def setUp(self):
438
-        if self.t is None:
439
-            self.__class__.t = tvdb_api.Tvdb(cache = True, banners = True)
440
+
441
+    @classmethod
442
+    def setup_class(cls):
443
+        if cls.t is None:
444
+            cls.t = tvdb_api.Tvdb(cache=True, banners=True)
445
 
446
     def test_have_banners(self):
447
         """Check banners at least one banner is found
448
         """
449
-        self.assertEquals(
450
-            len(self.t['scrubs']['_banners']) > 0,
451
-            True
452
-        )
453
+        assert (len(self.t['scrubs']['_banners']) > 0) == True
454
 
455
     def test_banner_url(self):
456
         """Checks banner URLs start with http://
457
         """
458
         for banner_type, banner_data in self.t['scrubs']['_banners'].items():
459
             for res, res_data in banner_data.items():
460
-                for bid, banner_info in res_data.items():
461
-                    self.assertEquals(
462
-                        banner_info['_bannerpath'].startswith("http://"),
463
-                        True
464
-                    )
465
+                if res != 'raw':
466
+                    for bid, banner_info in res_data.items():
467
+                        assert banner_info['_bannerpath'].startswith("http://") == True
468
 
469
+    @pytest.mark.skip('В новом API нет картинки у эпизода')
470
     def test_episode_image(self):
471
         """Checks episode 'filename' image is fully qualified URL
472
         """
473
-        self.assertEquals(
474
-            self.t['scrubs'][1][1]['filename'].startswith("http://"),
475
-            True
476
-        )
477
-    
478
+        assert self.t['scrubs'][1][1]['filename'].startswith("http://") == True
479
+
480
+    @pytest.mark.skip('В новом API у сериала кроме банера больше нет картинок')
481
     def test_show_artwork(self):
482
         """Checks various image URLs within season data are fully qualified
483
         """
484
         for key in ['banner', 'fanart', 'poster']:
485
-            self.assertEquals(
486
-                self.t['scrubs'][key].startswith("http://"),
487
-                True
488
-            )
489
+            assert self.t['scrubs'][key].startswith("http://") == True
490
+
491
 
492
-class test_tvdb_actors(unittest.TestCase):
493
+class TestTvdbActors:
494
     t = None
495
-    def setUp(self):
496
-        if self.t is None:
497
-            self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True)
498
+
499
+    @classmethod
500
+    def setup_class(cls):
501
+        if cls.t is None:
502
+            cls.t = tvdb_api.Tvdb(cache=True, actors=True)
503
 
504
     def test_actors_is_correct_datatype(self):
505
         """Check show/_actors key exists and is correct type"""
506
-        self.assertTrue(
507
-            isinstance(
508
-                self.t['scrubs']['_actors'],
509
-                tvdb_api.Actors
510
-            )
511
-        )
512
-    
513
+        assert isinstance(self.t['scrubs']['_actors'], tvdb_api.Actors) == True
514
+
515
     def test_actors_has_actor(self):
516
         """Check show has at least one Actor
517
         """
518
-        self.assertTrue(
519
-            isinstance(
520
-                self.t['scrubs']['_actors'][0],
521
-                tvdb_api.Actor
522
-            )
523
-        )
524
-    
525
+        assert isinstance(self.t['scrubs']['_actors'][0], tvdb_api.Actor) == True
526
+
527
     def test_actor_has_name(self):
528
         """Check first actor has a name"""
529
-        self.assertEquals(
530
-            self.t['scrubs']['_actors'][0]['name'],
531
-            "Zach Braff"
532
-        )
533
+        names = [actor['name'] for actor in self.t['scrubs']['_actors']]
534
+
535
+        assert u"Zach Braff" in names
536
 
537
     def test_actor_image_corrected(self):
538
         """Check image URL is fully qualified
539
@@ -419,71 +324,38 @@
540
             if actor['image'] is not None:
541
                 # Actor's image can be None, it displays as the placeholder
542
                 # image on thetvdb.com
543
-                self.assertTrue(
544
-                    actor['image'].startswith("http://")
545
-                )
546
+                assert actor['image'].startswith("http://") == True
547
 
548
-class test_tvdb_doctest(unittest.TestCase):
549
-    # Used to store the cached instance of Tvdb()
550
-    t = None
551
-    
552
-    def setUp(self):
553
-        if self.t is None:
554
-            self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
555
-    
556
+
557
+class TestTvdbDoctest:
558
     def test_doctest(self):
559
         """Check docstring examples works"""
560
         import doctest
561
         doctest.testmod(tvdb_api)
562
 
563
 
564
-class test_tvdb_custom_caching(unittest.TestCase):
565
+class TestTvdbCustomCaching:
566
     def test_true_false_string(self):
567
         """Tests setting cache to True/False/string
568
 
569
         Basic tests, only checking for errors
570
         """
571
-
572
-        tvdb_api.Tvdb(cache = True)
573
-        tvdb_api.Tvdb(cache = False)
574
-        tvdb_api.Tvdb(cache = "/tmp")
575
+        tvdb_api.Tvdb(cache=True)
576
+        tvdb_api.Tvdb(cache=False)
577
+        tvdb_api.Tvdb(cache="/tmp")
578
 
579
     def test_invalid_cache_option(self):
580
         """Tests setting cache to invalid value
581
         """
582
 
583
         try:
584
-            tvdb_api.Tvdb(cache = 2.3)
585
+            tvdb_api.Tvdb(cache=2.3)
586
         except ValueError:
587
             pass
588
         else:
589
-            self.fail("Expected ValueError from setting cache to float")
590
-
591
-    def test_custom_urlopener(self):
592
-        if not IS_PY2:
593
-            raise unittest.SkipTest("cannot supply custom opener in Python 3 because requests is used")
594
-
595
-        class UsedCustomOpener(Exception):
596
-            pass
597
-
598
-        import urllib2
599
-        class TestOpener(urllib2.BaseHandler):
600
-            def default_open(self, request):
601
-                print(request.get_method())
602
-                raise UsedCustomOpener("Something")
603
-
604
-        custom_opener = urllib2.build_opener(TestOpener())
605
-        t = tvdb_api.Tvdb(cache = custom_opener)
606
-        try:
607
-            t['scrubs']
608
-        except UsedCustomOpener:
609
-            pass
610
-        else:
611
-            self.fail("Did not use custom opener")
612
+            pytest.fail("Expected ValueError from setting cache to float")
613
 
614
     def test_custom_request_session(self):
615
-        if IS_PY2:
616
-            return
617
         from requests import Session as OriginalSession
618
         class Used(Exception):
619
             pass
620
@@ -492,103 +364,82 @@
621
             def request(self, *args, **kwargs):
622
                 raise Used("Hurray")
623
         c = CustomCacheForTest()
624
-        t = tvdb_api.Tvdb(cache = c)
625
+        t = tvdb_api.Tvdb(cache=c)
626
         try:
627
             t['scrubs']
628
         except Used:
629
             pass
630
         else:
631
-            self.fail("Did not use custom session")
632
+            pytest.fail("Did not use custom session")
633
 
634
 
635
-class test_tvdb_by_id(unittest.TestCase):
636
+class TestTvdbById:
637
     t = None
638
-    def setUp(self):
639
-        if self.t is None:
640
-            self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True)
641
+
642
+    @classmethod
643
+    def setup_class(cls):
644
+        if cls.t is None:
645
+            cls.t = tvdb_api.Tvdb(cache=True, actors=True)
646
 
647
     def test_actors_is_correct_datatype(self):
648
         """Check show/_actors key exists and is correct type"""
649
-        self.assertEquals(
650
-            self.t[76156]['seriesname'],
651
-            'Scrubs'
652
-            )
653
-
654
-
655
-class test_tvdb_zip(unittest.TestCase):
656
-    # Used to store the cached instance of Tvdb()
657
-    t = None
658
-
659
-    def setUp(self):
660
-        if self.t is None:
661
-            self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True)
662
-
663
-    def test_get_series_from_zip(self):
664
-        """
665
-        """
666
-        self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady')
667
-        self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs')
668
-
669
-    def test_spaces_from_zip(self):
670
-        """Checks shownames with spaces
671
-        """
672
-        self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl')
673
-        self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death')
674
+        assert self.t[76156]['seriesName'] == 'Scrubs'
675
 
676
 
677
-class test_tvdb_show_ordering(unittest.TestCase):
678
+class TestTvdbShowOrdering:
679
     # Used to store the cached instance of Tvdb()
680
     t_dvd = None
681
     t_air = None
682
 
683
-    def setUp(self):
684
-        if self.t_dvd is None:
685
-            self.t_dvd = tvdb_api.Tvdb(cache = True, useZip = True, dvdorder=True)
686
+    @classmethod
687
+    def setup_class(cls):
688
+        if cls.t_dvd is None:
689
+            cls.t_dvd = tvdb_api.Tvdb(cache=True, dvdorder=True)
690
 
691
-        if self.t_air is None:
692
-            self.t_air = tvdb_api.Tvdb(cache = True, useZip = True)
693
+        if cls.t_air is None:
694
+            cls.t_air = tvdb_api.Tvdb(cache=True)
695
 
696
     def test_ordering(self):
697
         """Test Tvdb.search method
698
         """
699
-        self.assertEquals(u'The Train Job', self.t_air['Firefly'][1][1]['episodename'])
700
-        self.assertEquals(u'Serenity', self.t_dvd['Firefly'][1][1]['episodename'])
701
+        assert u'The Train Job' == self.t_air['Firefly'][1][1]['episodeName']
702
+        assert u'Serenity' == self.t_dvd['Firefly'][1][1]['episodeName']
703
+
704
+        assert u'The Cat & the Claw (Part 1)' == self.t_air['Batman The Animated Series'][1][1]['episodeName']
705
+        assert u'On Leather Wings' == self.t_dvd['Batman The Animated Series'][1][1]['episodeName']
706
 
707
-        self.assertEquals(u'The Cat & the Claw (Part 1)', self.t_air['Batman The Animated Series'][1][1]['episodename'])
708
-        self.assertEquals(u'On Leather Wings', self.t_dvd['Batman The Animated Series'][1][1]['episodename'])
709
 
710
-class test_tvdb_show_search(unittest.TestCase):
711
+class TestTvdbShowSearch:
712
     # Used to store the cached instance of Tvdb()
713
     t = None
714
 
715
-    def setUp(self):
716
-        if self.t is None:
717
-            self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True)
718
+    @classmethod
719
+    def setup_class(cls):
720
+        if cls.t is None:
721
+            cls.t = tvdb_api.Tvdb(cache=True)
722
 
723
     def test_search(self):
724
         """Test Tvdb.search method
725
         """
726
         results = self.t.search("my name is earl")
727
-        all_ids = [x['seriesid'] for x in results]
728
-        self.assertTrue('75397' in all_ids)
729
+        all_ids = [x['id'] for x in results]
730
+        assert 75397 in all_ids
731
 
732
 
733
-class test_tvdb_alt_names(unittest.TestCase):
734
+class TestTvdbAltNames:
735
     t = None
736
-    def setUp(self):
737
-        if self.t is None:
738
-            self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True)
739
+
740
+    @classmethod
741
+    def setup_class(cls):
742
+        if cls.t is None:
743
+            cls.t = tvdb_api.Tvdb(cache=True, actors=True)
744
 
745
     def test_1(self):
746
         """Tests basic access of series name alias
747
         """
748
         results = self.t.search("Don't Trust the B---- in Apartment 23")
749
         series = results[0]
750
-        self.assertTrue(
751
-            'Apartment 23' in series['aliasnames']
752
-        )
753
-
754
+        assert 'Apartment 23' in series['aliases']
755
 
756
 if __name__ == '__main__':
757
-    runner = unittest.TextTestRunner(verbosity = 2)
758
-    unittest.main(testRunner = runner)
759
+    pytest.main()
760
tvdb_api-1.10.tar.gz/tvdb_api.egg-info/PKG-INFO -> tvdb_api-2.0.tar.gz/tvdb_api.egg-info/PKG-INFO Changed
27
 
1
@@ -1,6 +1,6 @@
2
 Metadata-Version: 1.1
3
 Name: tvdb-api
4
-Version: 1.10
5
+Version: 2.0
6
 Summary: Interface to thetvdb.com
7
 Home-page: http://github.com/dbr/tvdb_api/tree/master
8
 Author: dbr/Ben
9
@@ -14,7 +14,7 @@
10
         >>> ep = t['My Name Is Earl'][1][22]
11
         >>> ep
12
         <Episode 01x22 - Stole a Badge>
13
-        >>> ep['episodename']
14
+        >>> ep['episodeName']
15
         u'Stole a Badge'
16
         
17
 Platform: UNKNOWN
18
@@ -27,6 +27,8 @@
19
 Classifier: Programming Language :: Python :: 2.7
20
 Classifier: Programming Language :: Python :: 3.3
21
 Classifier: Programming Language :: Python :: 3.4
22
+Classifier: Programming Language :: Python :: 3.5
23
+Classifier: Programming Language :: Python :: 3.6
24
 Classifier: Topic :: Multimedia
25
 Classifier: Topic :: Utilities
26
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
tvdb_api-1.10.tar.gz/tvdb_api.egg-info/SOURCES.txt -> tvdb_api-2.0.tar.gz/tvdb_api.egg-info/SOURCES.txt Changed
16
 
1
@@ -4,7 +4,6 @@
2
 readme.md
3
 setup.py
4
 tvdb_api.py
5
-tvdb_cache.py
6
 tvdb_exceptions.py
7
 tvdb_ui.py
8
 tests/gprof2dot.py
9
@@ -13,4 +12,5 @@
10
 tvdb_api.egg-info/PKG-INFO
11
 tvdb_api.egg-info/SOURCES.txt
12
 tvdb_api.egg-info/dependency_links.txt
13
+tvdb_api.egg-info/requires.txt
14
 tvdb_api.egg-info/top_level.txt
15
\ No newline at end of file
16
tvdb_api-2.0.tar.gz/tvdb_api.egg-info/requires.txt Added
4
 
1
@@ -0,0 +1,2 @@
2
+requests_cache
3
+requests
4
tvdb_api-1.10.tar.gz/tvdb_api.egg-info/top_level.txt -> tvdb_api-2.0.tar.gz/tvdb_api.egg-info/top_level.txt Changed
7
 
1
@@ -1,4 +1,3 @@
2
 tvdb_api
3
-tvdb_ui
4
 tvdb_exceptions
5
-tvdb_cache
6
+tvdb_ui
7
tvdb_api-1.10.tar.gz/tvdb_api.py -> tvdb_api-2.0.tar.gz/tvdb_api.py Changed
1460
 
1
@@ -1,9 +1,9 @@
2
 #!/usr/bin/env python
3
-#encoding:utf-8
4
-#author:dbr/Ben
5
-#project:tvdb_api
6
-#repository:http://github.com/dbr/tvdb_api
7
-#license:unlicense (http://unlicense.org/)
8
+# encoding:utf-8
9
+# author:dbr/Ben
10
+# project:tvdb_api
11
+# repository:http://github.com/dbr/tvdb_api
12
+# license:unlicense (http://unlicense.org/)
13
 
14
 """Simple-to-use Python interface to The TVDB's API (thetvdb.com)
15
 
16
@@ -11,42 +11,38 @@
17
 
18
 >>> from tvdb_api import Tvdb
19
 >>> t = Tvdb()
20
->>> t['Lost'][4][11]['episodename']
21
+>>> t['Lost'][4][11]['episodeName']
22
 u'Cabin Fever'
23
 """
24
-__author__ = "dbr/Ben"
25
-__version__ = "1.10"
26
 
27
-import sys
28
+__author__ = "dbr/Ben"
29
+__version__ = "2.0"
30
 
31
-IS_PY2 = sys.version_info[0] == 2
32
 
33
+import sys
34
 import os
35
 import time
36
-if IS_PY2:
37
-    import urllib
38
-    import urllib2
39
-    from tvdb_cache import CacheHandler
40
-    from urllib import quote as url_quote
41
-else:
42
-    import requests
43
-    from urllib.parse import quote as url_quote
44
+import types
45
 import getpass
46
 import tempfile
47
 import warnings
48
 import logging
49
 import datetime
50
-import zipfile
51
+import hashlib
52
 
53
-try:
54
-    import xml.etree.cElementTree as ElementTree
55
-except ImportError:
56
-    import xml.etree.ElementTree as ElementTree
57
+import requests
58
+import requests_cache
59
+from requests_cache.backends.base import _to_bytes, _DEFAULT_HEADERS
60
 
61
-try:
62
-    import gzip
63
-except ImportError:
64
-    gzip = None
65
+
66
+IS_PY2 = sys.version_info[0] == 2
67
+
68
+if IS_PY2:
69
+    user_input = raw_input
70
+    from urllib import quote as url_quote
71
+else:
72
+    from urllib.parse import quote as url_quote
73
+    user_input = input
74
 
75
 
76
 if IS_PY2:
77
@@ -56,17 +52,217 @@
78
     int_types = int
79
     text_type = str
80
 
81
-
82
-from tvdb_ui import BaseUI, ConsoleUI
83
-from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
84
-    tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
85
-
86
 lastTimeout = None
87
 
88
+
89
 def log():
90
     return logging.getLogger("tvdb_api")
91
 
92
 
93
+## Exceptions
94
+
95
+class tvdb_exception(Exception):
96
+    """Any exception generated by tvdb_api
97
+    """
98
+    pass
99
+
100
+class tvdb_error(tvdb_exception):
101
+    """An error with thetvdb.com (Cannot connect, for example)
102
+    """
103
+    pass
104
+
105
+class tvdb_userabort(tvdb_exception):
106
+    """User aborted the interactive selection (via
107
+    the q command, ^c etc)
108
+    """
109
+    pass
110
+
111
+class tvdb_notauthorized(tvdb_exception):
112
+    """An authorization error with thetvdb.com
113
+    """
114
+    pass
115
+
116
+class tvdb_shownotfound(tvdb_exception):
117
+    """Show cannot be found on thetvdb.com (non-existant show)
118
+    """
119
+    pass
120
+
121
+class tvdb_seasonnotfound(tvdb_exception):
122
+    """Season cannot be found on thetvdb.com
123
+    """
124
+    pass
125
+
126
+class tvdb_episodenotfound(tvdb_exception):
127
+    """Episode cannot be found on thetvdb.com
128
+    """
129
+    pass
130
+
131
+class tvdb_resourcenotfound(tvdb_exception):
132
+    """Resource cannot be found on thetvdb.com
133
+    """
134
+    pass
135
+
136
+class tvdb_invalidlanguage(tvdb_exception):
137
+    """invalid language given on thetvdb.com
138
+    """
139
+    def __init__(self, value):
140
+        self.value = value
141
+
142
+    def __str__(self):
143
+        return repr(self.value)
144
+
145
+class tvdb_attributenotfound(tvdb_exception):
146
+    """Raised if an episode does not have the requested
147
+    attribute (such as a episode name)
148
+    """
149
+    pass
150
+
151
+
152
+## UI
153
+
154
+class BaseUI(object):
155
+    """Base user interface for Tvdb show selection.
156
+
157
+    Selects first show.
158
+
159
+    A UI is a callback. A class, it's __init__ function takes two arguments:
160
+
161
+    - config, which is the Tvdb config dict, setup in tvdb_api.py
162
+    - log, which is Tvdb's logger instance (which uses the logging module). You can
163
+    call log.info() log.warning() etc
164
+
165
+    It must have a method "selectSeries", this is passed a list of dicts, each dict
166
+    contains the the keys "name" (human readable show name), and "sid" (the shows
167
+    ID as on thetvdb.com). For example:
168
+
169
+    [{'name': u'Lost', 'sid': u'73739'},
170
+     {'name': u'Lost Universe', 'sid': u'73181'}]
171
+
172
+    The "selectSeries" method must return the appropriate dict, or it can raise
173
+    tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show
174
+    cannot be found).
175
+
176
+    A simple example callback, which returns a random series:
177
+
178
+    >>> import random
179
+    >>> from tvdb_ui import BaseUI
180
+    >>> class RandomUI(BaseUI):
181
+    ...    def selectSeries(self, allSeries):
182
+    ...            import random
183
+    ...            return random.choice(allSeries)
184
+
185
+    Then to use it..
186
+
187
+    >>> from tvdb_api import Tvdb
188
+    >>> t = Tvdb(custom_ui = RandomUI)
189
+    >>> random_matching_series = t['Lost']
190
+    >>> type(random_matching_series)
191
+    <class 'tvdb_api.Show'>
192
+    """
193
+    def __init__(self, config, log = None):
194
+        self.config = config
195
+        if log is not None:
196
+            warnings.warn("the UI's log parameter is deprecated, instead use\n"
197
+                "use import logging; logging.getLogger('ui').info('blah')\n"
198
+                "The self.log attribute will be removed in the next version")
199
+            self.log = logging.getLogger(__name__)
200
+
201
+    def selectSeries(self, allSeries):
202
+        return allSeries[0]
203
+
204
+
205
+class ConsoleUI(BaseUI):
206
+    """Interactively allows the user to select a show from a console based UI
207
+    """
208
+
209
+    def _displaySeries(self, allSeries, limit = 6):
210
+        """Helper function, lists series with corresponding ID
211
+        """
212
+        if limit is not None:
213
+            toshow = allSeries[:limit]
214
+        else:
215
+            toshow = allSeries
216
+
217
+        print("TVDB Search Results:")
218
+        for i, cshow in enumerate(toshow):
219
+            i_show = i + 1 # Start at more human readable number 1 (not 0)
220
+            log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesName']))
221
+            if i == 0:
222
+                extra = " (default)"
223
+            else:
224
+                extra = ""
225
+
226
+            lid_map = dict((v, k) for (k, v) in self.config['langabbv_to_id'].items())
227
+
228
+            output = "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % (
229
+                i_show,
230
+                cshow['seriesName'],
231
+                lid_map[cshow['lid']],
232
+                str(cshow['id']),
233
+                cshow['lid'],
234
+                extra
235
+            )
236
+            if IS_PY2:
237
+                print(output.encode("UTF-8", "ignore"))
238
+            else:
239
+                print(output)
240
+
241
+    def selectSeries(self, allSeries):
242
+        self._displaySeries(allSeries)
243
+
244
+        if len(allSeries) == 1:
245
+            # Single result, return it!
246
+            print("Automatically selecting only result")
247
+            return allSeries[0]
248
+
249
+        if self.config['select_first'] is True:
250
+            print("Automatically returning first search result")
251
+            return allSeries[0]
252
+
253
+        while True: # return breaks this loop
254
+            try:
255
+                print("Enter choice (first number, return for default, 'all', ? for help):")
256
+                ans = user_input()
257
+            except KeyboardInterrupt:
258
+                raise tvdb_userabort("User aborted (^c keyboard interupt)")
259
+            except EOFError:
260
+                raise tvdb_userabort("User aborted (EOF received)")
261
+
262
+            log().debug('Got choice of: %s' % (ans))
263
+            try:
264
+                selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
265
+            except ValueError: # Input was not number
266
+                if len(ans.strip()) == 0:
267
+                    # Default option
268
+                    log().debug('Default option, returning first series')
269
+                    return allSeries[0]
270
+                if ans == "q":
271
+                    log().debug('Got quit command (q)')
272
+                    raise tvdb_userabort("User aborted ('q' quit command)")
273
+                elif ans == "?":
274
+                    print("## Help")
275
+                    print("# Enter the number that corresponds to the correct show.")
276
+                    print("# a - display all results")
277
+                    print("# all - display all results")
278
+                    print("# ? - this help")
279
+                    print("# q - abort tvnamer")
280
+                    print("# Press return with no input to select first result")
281
+                elif ans.lower() in ["a", "all"]:
282
+                    self._displaySeries(allSeries, limit = None)
283
+                else:
284
+                    log().debug('Unknown keypress %s' % (ans))
285
+            else:
286
+                log().debug('Trying to return ID: %d' % (selected_id))
287
+                try:
288
+                    return allSeries[selected_id]
289
+                except IndexError:
290
+                    log().debug('Invalid show number entered!')
291
+                    print("Invalid number (%s) selected!")
292
+                    self._displaySeries(allSeries)
293
+
294
+
295
+## Main API
296
+
297
 class ShowContainer(dict):
298
     """Simple dict that holds a series of Show instances
299
     """
300
@@ -78,7 +274,7 @@
301
     def __setitem__(self, key, value):
302
         self._stack.append(key)
303
 
304
-        #keep only the 100th latest results
305
+        # keep only the 100th latest results
306
         if time.time() - self._lastgc > 20:
307
             for o in self._stack[:-100]:
308
                 del self[o]
309
@@ -97,12 +293,22 @@
310
         self.data = {}
311
 
312
     def __repr__(self):
313
-        return "<Show %s (containing %s seasons)>" % (
314
-            self.data.get(u'seriesname', 'instance'),
315
+        return "<Show %r (containing %s seasons)>" % (
316
+            self.data.get(u'seriesName', 'instance'),
317
             len(self)
318
         )
319
 
320
     def __getitem__(self, key):
321
+        v1_compatibility = {
322
+            'seriesname': 'seriesName',
323
+        }
324
+
325
+        if key in v1_compatibility:
326
+            import warnings
327
+            msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % (
328
+                key, v1_compatibility[key])
329
+            key = v1_compatibility[key]
330
+
331
         if key in self:
332
             # Key is an episode, return it
333
             return dict.__getitem__(self, key)
334
@@ -114,22 +320,34 @@
335
         # Data wasn't found, raise appropriate error
336
         if isinstance(key, int) or key.isdigit():
337
             # Episode number x was not found
338
-            raise tvdb_seasonnotfound("Could not find season %s" % (repr(key)))
339
+            raise tvdb_seasonnotfound(
340
+                "Could not find season %s" % (repr(key))
341
+            )
342
         else:
343
             # If it's not numeric, it must be an attribute name, which
344
             # doesn't exist, so attribute error.
345
-            raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
346
+            raise tvdb_attributenotfound(
347
+                "Cannot find attribute %s" % (repr(key))
348
+            )
349
 
350
     def airedOn(self, date):
351
-        ret = self.search(str(date), 'firstaired')
352
+        """Deprecated: use aired_on instead
353
+        """
354
+        warnings.warn("Show.airedOn method renamed to aired_on", category=DeprecationWarning)
355
+        return self.aired_on(date)
356
+
357
+    def aired_on(self, date):
358
+        ret = self.search(str(date), 'firstAired')
359
         if len(ret) == 0:
360
-            raise tvdb_episodenotfound("Could not find any episodes that aired on %s" % date)
361
+            raise tvdb_episodenotfound(
362
+                "Could not find any episodes that aired on %s" % date
363
+            )
364
         return ret
365
 
366
-    def search(self, term = None, key = None):
367
+    def search(self, term=None, key=None):
368
         """
369
-        Search all episodes in show. Can search all data, or a specific key (for
370
-        example, episodename)
371
+        Search all episodes in show. Can search all data, or a specific key
372
+        (for example, episodename)
373
 
374
         Always returns an array (can be empty). First index contains the first
375
         match, and so on.
376
@@ -141,9 +359,9 @@
377
         Search terms are converted to lower case (unicode) strings.
378
 
379
         # Examples
380
-        
381
+
382
         These examples assume t is an instance of Tvdb():
383
-        
384
+
385
         >>> t = Tvdb()
386
         >>>
387
 
388
@@ -151,27 +369,27 @@
389
         containing "my first day":
390
 
391
         >>> t['Scrubs'].search("my first day")
392
-        [<Episode 01x01 - My First Day>]
393
+        [<Episode 01x01 - u'My First Day'>]
394
         >>>
395
 
396
         Search for "My Name Is Earl" episode named "Faked His Own Death":
397
 
398
-        >>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
399
-        [<Episode 01x04 - Faked His Own Death>]
400
+        >>> t['My Name Is Earl'].search('Faked My Own Death', key='episodeName')
401
+        [<Episode 01x04 - u'Faked My Own Death'>]
402
         >>>
403
 
404
         To search Scrubs for all episodes with "mentor" in the episode name:
405
 
406
-        >>> t['scrubs'].search('mentor', key = 'episodename')
407
-        [<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
408
+        >>> t['scrubs'].search('mentor', key='episodeName')
409
+        [<Episode 01x02 - u'My Mentor'>, <Episode 03x15 - u'My Tormented Mentor'>]
410
         >>>
411
 
412
         # Using search results
413
 
414
         >>> results = t['Scrubs'].search("my first")
415
-        >>> print results[0]['episodename']
416
+        >>> print results[0]['episodeName']
417
         My First Day
418
-        >>> for x in results: print x['episodename']
419
+        >>> for x in results: print x['episodeName']
420
         My First Day
421
         My First Step
422
         My First Kill
423
@@ -179,7 +397,7 @@
424
         """
425
         results = []
426
         for cur_season in self.values():
427
-            searchresult = cur_season.search(term = term, key = key)
428
+            searchresult = cur_season.search(term=term, key=key)
429
             if len(searchresult) != 0:
430
                 results.extend(searchresult)
431
 
432
@@ -187,7 +405,7 @@
433
 
434
 
435
 class Season(dict):
436
-    def __init__(self, show = None):
437
+    def __init__(self, show=None):
438
         """The show attribute points to the parent show
439
         """
440
         self.show = show
441
@@ -203,20 +421,20 @@
442
         else:
443
             return dict.__getitem__(self, episode_number)
444
 
445
-    def search(self, term = None, key = None):
446
+    def search(self, term=None, key=None):
447
         """Search all episodes in season, returns a list of matching Episode
448
         instances.
449
 
450
         >>> t = Tvdb()
451
         >>> t['scrubs'][1].search('first day')
452
-        [<Episode 01x01 - My First Day>]
453
+        [<Episode 01x01 - u'My First Day'>]
454
         >>>
455
 
456
         See Show.search documentation for further information on search
457
         """
458
         results = []
459
         for ep in self.values():
460
-            searchresult = ep.search(term = term, key = key)
461
+            searchresult = ep.search(term=term, key=key)
462
             if searchresult is not None:
463
                 results.append(
464
                     searchresult
465
@@ -225,17 +443,17 @@
466
 
467
 
468
 class Episode(dict):
469
-    def __init__(self, season = None):
470
+    def __init__(self, season=None):
471
         """The season attribute points to the parent season
472
         """
473
         self.season = season
474
 
475
     def __repr__(self):
476
-        seasno = int(self.get(u'seasonnumber', 0))
477
-        epno = int(self.get(u'episodenumber', 0))
478
-        epname = self.get(u'episodename')
479
+        seasno = self.get(u'airedSeason', 0)
480
+        epno = self.get(u'airedEpisodeNumber', 0)
481
+        epname = self.get(u'episodeName')
482
         if epname is not None:
483
-            return "<Episode %02dx%02d - %s>" % (seasno, epno, epname)
484
+            return "<Episode %02dx%02d - %r>" % (seasno, epno, epname)
485
         else:
486
             return "<Episode %02dx%02d>" % (seasno, epno)
487
 
488
@@ -243,40 +461,62 @@
489
         try:
490
             return dict.__getitem__(self, key)
491
         except KeyError:
492
-            raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
493
+            v1_compatibility = {
494
+                'episodenumber': 'airedEpisodeNumber',
495
+                'firstaired': 'firstAired',
496
+                'seasonnumber': 'airedSeason',
497
+                'episodename': 'episodeName',
498
+            }
499
+            if key in v1_compatibility:
500
+                msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % (
501
+                    key, v1_compatibility[key])
502
+                warnings.warn(msg, category=DeprecationWarning)
503
+                try:
504
+                    value = dict.__getitem__(self, v1_compatibility[key])
505
+                    if key in ['episodenumber', 'seasonnumber']:
506
+                        # This was a string in v1
507
+                        return str(value)
508
+                    else:
509
+                        return value
510
+                except KeyError:
511
+                    # We either return something or we get the exception below
512
+                    pass
513
+
514
+                    raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
515
 
516
-    def search(self, term = None, key = None):
517
+    def search(self, term=None, key=None):
518
         """Search episode data for term, if it matches, return the Episode (self).
519
         The key parameter can be used to limit the search to a specific element,
520
         for example, episodename.
521
-        
522
+
523
         This primarily for use use by Show.search and Season.search. See
524
         Show.search for further information on search
525
 
526
         Simple example:
527
 
528
         >>> e = Episode()
529
-        >>> e['episodename'] = "An Example"
530
+        >>> e['episodeName'] = "An Example"
531
         >>> e.search("examp")
532
-        <Episode 00x00 - An Example>
533
+        <Episode 00x00 - 'An Example'>
534
         >>>
535
 
536
         Limiting by key:
537
 
538
-        >>> e.search("examp", key = "episodename")
539
-        <Episode 00x00 - An Example>
540
+        >>> e.search("examp", key = "episodeName")
541
+        <Episode 00x00 - 'An Example'>
542
         >>>
543
         """
544
-        if term == None:
545
+        if term is None:
546
             raise TypeError("must supply string to search for (contents)")
547
 
548
         term = text_type(term).lower()
549
         for cur_key, cur_value in self.items():
550
-            cur_key, cur_value = text_type(cur_key).lower(), text_type(cur_value).lower()
551
+            cur_key = text_type(cur_key)
552
+            cur_value = text_type(cur_value).lower()
553
             if key is not None and cur_key != key:
554
                 # Do not search this key
555
                 continue
556
-            if cur_value.find( text_type(term).lower() ) > -1:
557
+            if cur_value.find(text_type(term)) > -1:
558
                 return self
559
 
560
 
561
@@ -296,29 +536,65 @@
562
     sortorder
563
     """
564
     def __repr__(self):
565
-        return "<Actor \"%s\">" % (self.get("name"))
566
+        return "<Actor %r>" % self.get("name")
567
+
568
+
569
+def create_key(self, request):
570
+    """A new cache_key algo is required as the authentication token
571
+    changes with each run. Also there are other header params which
572
+    also change with each request (e.g. timestamp). Excluding all
573
+    headers means that Accept-Language is excluded which means
574
+    different language requests will return the cached response from
575
+    the wrong language.
576
+
577
+    The _loadurl part checks the cache before a get is performed so
578
+    that an auth token can be obtained. If the response is already in
579
+    the cache, the auth token is not required. This prevents the need
580
+    to do a get which may access the host and fail because the session
581
+    is not yet not authorized. It is not necessary to authorize if the
582
+    cache is to be used thus saving host and network traffic.
583
+    """
584
+
585
+    if self._ignored_parameters:
586
+        url, body = self._remove_ignored_parameters(request)
587
+    else:
588
+        url, body = request.url, request.body
589
+    key = hashlib.sha256()
590
+    key.update(_to_bytes(request.method.upper()))
591
+    key.update(_to_bytes(url))
592
+    if request.body:
593
+        key.update(_to_bytes(body))
594
+    else:
595
+        if self._include_get_headers and request.headers != _DEFAULT_HEADERS:
596
+            for name, value in sorted(request.headers.items()):
597
+                # include only Accept-Language as it is important for context
598
+                if name in ['Accept-Language']:
599
+                    key.update(_to_bytes(name))
600
+                    key.update(_to_bytes(value))
601
+    return key.hexdigest()
602
 
603
 
604
 class Tvdb:
605
     """Create easy-to-use interface to name of season/episode name
606
     >>> t = Tvdb()
607
-    >>> t['Scrubs'][1][24]['episodename']
608
+    >>> t['Scrubs'][1][24]['episodeName']
609
     u'My Last Day'
610
     """
611
     def __init__(self,
612
-                interactive = False,
613
-                select_first = False,
614
-                debug = False,
615
-                cache = True,
616
-                banners = False,
617
-                actors = False,
618
-                custom_ui = None,
619
-                language = None,
620
-                search_all_languages = False,
621
-                apikey = None,
622
-                forceConnect=False,
623
-                useZip=False,
624
-                dvdorder=False):
625
+                 interactive=False,
626
+                 select_first=False,
627
+                 debug=False,
628
+                 cache=True,
629
+                 banners=False,
630
+                 actors=False,
631
+                 custom_ui=None,
632
+                 language=None,
633
+                 search_all_languages=False,
634
+                 apikey=None,
635
+                 username=None,
636
+                 userkey=None,
637
+                 forceConnect=False,
638
+                 dvdorder=False):
639
 
640
         """interactive (True/False):
641
             When True, uses built-in console UI is used to select the correct show.
642
@@ -335,26 +611,22 @@
643
                  >>> import logging
644
                  >>> logging.basicConfig(level = logging.DEBUG)
645
 
646
-        cache (True/False/str/unicode/urllib2 opener):
647
-            Retrieved XML are persisted to to disc. If true, stores in
648
-            tvdb_api folder under your systems TEMP_DIR, if set to
649
-            str/unicode instance it will use this as the cache
650
-            location. If False, disables caching.  Can also be passed
651
-            an arbitrary Python object, which is used as a urllib2
652
-            opener, which should be created by urllib2.build_opener
653
-
654
-            In Python 3, True/False enable or disable default
655
-            caching. Passing string specified directory where to store
656
-            the "tvdb.sqlite3" cache file. Also a custom
657
+        cache (True/False/str/requests_cache.CachedSession):
658
+
659
+            Retrieved URLs can be persisted to to disc.
660
+
661
+            True/False enable or disable default caching. Passing
662
+            string specifies the directory where to store the
663
+            "tvdb.sqlite3" cache file. Alternatively a custom
664
             requests.Session instance can be passed (e.g maybe a
665
-            customised instance of requests_cache.CachedSession)
666
+            customised instance of `requests_cache.CachedSession`)
667
 
668
         banners (True/False):
669
             Retrieves the banners for a show. These are accessed
670
             via the _banners key of a Show(), for example:
671
 
672
             >>> Tvdb(banners=True)['scrubs']['_banners'].keys()
673
-            ['fanart', 'poster', 'series', 'season']
674
+            [u'fanart', u'poster', u'seasonwide', u'season', u'series']
675
 
676
         actors (True/False):
677
             Retrieves a list of the actors for a show. These are accessed
678
@@ -362,7 +634,7 @@
679
 
680
             >>> t = Tvdb(actors=True)
681
             >>> t['scrubs']['_actors'][0]['name']
682
-            u'Zach Braff'
683
+            u'John C. McGinley'
684
 
685
         custom_ui (tvdb_ui.BaseUI subclass):
686
             A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
687
@@ -378,7 +650,7 @@
688
             By default, Tvdb will only search in the language specified using
689
             the language option. When this is True, it will search for the
690
             show in and language
691
-        
692
+
693
         apikey (str/unicode):
694
             Override the default thetvdb.com API key. By default it will use
695
             tvdb_api's own key (fine for small scripts), but you can use your
696
@@ -386,129 +658,123 @@
697
             tvdb_api in a larger application)
698
             See http://thetvdb.com/?tab=apiregister to get your own key
699
 
700
+        username (str/unicode):
701
+            Override the default thetvdb.com username. By default it will use
702
+            tvdb_api's own username (fine for small scripts), but you can use your
703
+            own key if desired - this is recommended if you are embedding
704
+            tvdb_api in a larger application)
705
+            See http://thetvdb.com/ to register an account
706
+
707
+        userkey (str/unicode):
708
+            Override the default thetvdb.com userkey. By default it will use
709
+            tvdb_api's own userkey (fine for small scripts), but you can use your
710
+            own key if desired - this is recommended if you are embedding
711
+            tvdb_api in a larger application)
712
+            See http://thetvdb.com/ to register an account
713
+
714
         forceConnect (bool):
715
             If true it will always try to connect to theTVDB.com even if we
716
             recently timed out. By default it will wait one minute before
717
             trying again, and any requests within that one minute window will
718
             return an exception immediately.
719
-
720
-        useZip (bool):
721
-            Download the zip archive where possibale, instead of the xml.
722
-            This is only used when all episodes are pulled.
723
-            And only the main language xml is used, the actor and banner xml are lost.
724
         """
725
-        
726
 
727
         global lastTimeout
728
-        
729
+
730
         # if we're given a lastTimeout that is less than 1 min just give up
731
-        if not forceConnect and lastTimeout != None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1):
732
+        if not forceConnect and lastTimeout is not None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1):
733
             raise tvdb_error("We recently timed out, so giving up early this time")
734
-        
735
-        self.shows = ShowContainer() # Holds all Show classes
736
-        self.corrections = {} # Holds show-name to show_id mapping
737
+
738
+        self.shows = ShowContainer()  # Holds all Show classes
739
+        self.corrections = {}  # Holds show-name to show_id mapping
740
 
741
         self.config = {}
742
 
743
-        if apikey is not None:
744
-            self.config['apikey'] = apikey
745
+        if apikey and username and userkey:
746
+            self.config['auth_payload'] = {
747
+                "apikey": apikey,
748
+                "username": username,
749
+                "userkey": userkey
750
+            }
751
         else:
752
-            self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key
753
+            self.config['auth_payload'] = {
754
+                "apikey": "0629B785CE550C8D",
755
+                "userkey": "",
756
+                "username": ""
757
+            }
758
 
759
-        self.config['debug_enabled'] = debug # show debugging messages
760
+        self.config['debug_enabled'] = debug  # show debugging messages
761
 
762
         self.config['custom_ui'] = custom_ui
763
 
764
-        self.config['interactive'] = interactive # prompt for correct series?
765
+        self.config['interactive'] = interactive  # prompt for correct series?
766
 
767
         self.config['select_first'] = select_first
768
 
769
         self.config['search_all_languages'] = search_all_languages
770
 
771
-        self.config['useZip'] = useZip
772
-
773
         self.config['dvdorder'] = dvdorder
774
 
775
-        if not IS_PY2: # FIXME: Allow using requests in Python 2?
776
-            import requests_cache
777
-            if cache is True:
778
-                self.session = requests_cache.CachedSession(
779
-                    expire_after=21600, # 6 hours
780
-                    backend='sqlite',
781
-                    cache_name=self._getTempDir(),
782
-                    )
783
-                self.config['cache_enabled'] = True
784
-            elif cache is False:
785
-                self.session = requests.Session()
786
-                self.config['cache_enabled'] = False
787
-            elif isinstance(cache, text_type):
788
-                # Specified cache path
789
-                self.session = requests_cache.CachedSession(
790
-                    expire_after=21600, # 6 hours
791
-                    backend='sqlite',
792
-                    cache_name=os.path.join(cache, "tvdb_api"),
793
-                    )
794
-            else:
795
-                self.session = cache
796
-                try:
797
-                    self.session.get
798
-                except AttributeError:
799
-                    raise ValueError("cache argument must be True/False, string as cache path or requests.Session-type object (e.g from requests_cache.CachedSession)")
800
-        else:
801
-            # For backwards compatibility in Python 2.x
802
-            if cache is True:
803
-                self.config['cache_enabled'] = True
804
-                self.config['cache_location'] = self._getTempDir()
805
-                self.urlopener = urllib2.build_opener(
806
-                    CacheHandler(self.config['cache_location'])
807
+        if cache is True:
808
+            self.session = requests_cache.CachedSession(
809
+                expire_after=21600,  # 6 hours
810
+                backend='sqlite',
811
+                cache_name=self._getTempDir(),
812
+                include_get_headers=True
813
                 )
814
-
815
-            elif cache is False:
816
-                self.config['cache_enabled'] = False
817
-                self.urlopener = urllib2.build_opener() # default opener with no caching
818
-
819
-            elif isinstance(cache, basestring):
820
-                self.config['cache_enabled'] = True
821
-                self.config['cache_location'] = cache
822
-                self.urlopener = urllib2.build_opener(
823
-                    CacheHandler(self.config['cache_location'])
824
+            self.session.cache.create_key = types.MethodType(create_key, self.session.cache)
825
+            self.session.remove_expired_responses()
826
+            self.config['cache_enabled'] = True
827
+        elif cache is False:
828
+            self.session = requests.Session()
829
+            self.config['cache_enabled'] = False
830
+        elif isinstance(cache, str):
831
+            # Specified cache path
832
+            self.session = requests_cache.CachedSession(
833
+                expire_after=21600,  # 6 hours
834
+                backend='sqlite',
835
+                cache_name=os.path.join(cache, "tvdb_api"),
836
+                include_get_headers=True
837
                 )
838
-
839
-            elif isinstance(cache, urllib2.OpenerDirector):
840
-                # If passed something from urllib2.build_opener, use that
841
-                log().debug("Using %r as urlopener" % cache)
842
-                self.config['cache_enabled'] = True
843
-                self.urlopener = cache
844
-
845
-            else:
846
-                raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache)))
847
+            self.session.cache.create_key = types.MethodType(create_key, self.session.cache)
848
+            self.session.remove_expired_responses()
849
+        else:
850
+            self.session = cache
851
+            try:
852
+                self.session.get
853
+            except AttributeError:
854
+                raise ValueError("cache argument must be True/False, string as cache path or requests.Session-type object (e.g from requests_cache.CachedSession)")
855
 
856
         self.config['banners_enabled'] = banners
857
         self.config['actors_enabled'] = actors
858
 
859
         if self.config['debug_enabled']:
860
-            warnings.warn("The debug argument to tvdb_api.__init__ will be removed in the next version. "
861
-            "To enable debug messages, use the following code before importing: "
862
-            "import logging; logging.basicConfig(level=logging.DEBUG)")
863
+            warnings.warn(
864
+                "The debug argument to tvdb_api.__init__ will be removed in the next version. "
865
+                "To enable debug messages, use the following code before importing: "
866
+                "import logging; logging.basicConfig(level=logging.DEBUG)"
867
+            )
868
             logging.basicConfig(level=logging.DEBUG)
869
 
870
-
871
         # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml
872
         # Hard-coded here as it is realtively static, and saves another HTTP request, as
873
         # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
874
         self.config['valid_languages'] = [
875
-            "da", "fi", "nl", "de", "it", "es", "fr","pl", "hu","el","tr",
876
-            "ru","he","ja","pt","zh","cs","sl", "hr","ko","en","sv","no"
877
+            "da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr",
878
+            "ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv",
879
+            "no"
880
         ]
881
 
882
         # thetvdb.com should be based around numeric language codes,
883
         # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16
884
         # requires the language ID, thus this mapping is required (mainly
885
         # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations)
886
-        self.config['langabbv_to_id'] = {'el': 20, 'en': 7, 'zh': 27,
887
-        'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9,
888
-        'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11,
889
-        'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30}
890
+        self.config['langabbv_to_id'] = {
891
+            'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16,
892
+            'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18,
893
+            'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19,
894
+            'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30
895
+        }
896
 
897
         if language is None:
898
             self.config['language'] = 'en'
899
@@ -523,21 +789,22 @@
900
         # The following url_ configs are based of the
901
         # http://thetvdb.com/wiki/index.php/Programmers_API
902
         self.config['base_url'] = "http://thetvdb.com"
903
+        self.config['api_url'] = "https://api.thetvdb.com"
904
 
905
-        if self.config['search_all_languages']:
906
-            self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config
907
-        else:
908
-            self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % self.config
909
+        self.config['url_getSeries'] = u"%(api_url)s/search/series?name=%%s" % self.config
910
 
911
-        self.config['url_epInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml" % self.config
912
-        self.config['url_epInfo_zip'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip" % self.config
913
+        self.config['url_epInfo'] = u"%(api_url)s/series/%%s/episodes" % self.config
914
 
915
-        self.config['url_seriesInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml" % self.config
916
-        self.config['url_actorsInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config
917
+        self.config['url_seriesInfo'] = u"%(api_url)s/series/%%s" % self.config
918
+        self.config['url_actorsInfo'] = u"%(api_url)s/series/%%s/actors" % self.config
919
 
920
-        self.config['url_seriesBanner'] = u"%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config
921
+        self.config['url_seriesBanner'] = u"%(api_url)s/series/%%s/images" % self.config
922
+        self.config['url_seriesBannerInfo'] = u"%(api_url)s/series/%%s/images/query?keyType=%%s" % self.config
923
         self.config['url_artworkPrefix'] = u"%(base_url)s/banners/%%s" % self.config
924
 
925
+        self.__authorized = False
926
+        self.headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Accept-Language': self.config['language']}
927
+
928
     def _getTempDir(self):
929
         """Returns the [system temp dir]/tvdb_api-u501 (or
930
         tvdb_api-myuser)
931
@@ -553,101 +820,89 @@
932
 
933
         return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid))
934
 
935
-    def _loadUrl(self, url, recache = False, language=None):
936
-        if not IS_PY2:
937
-            # Python 3: return content at URL as bytes
938
-            resp = self.session.get(url)
939
-            if 'application/zip' in resp.headers.get("Content-Type", ''):
940
-                try:
941
-                    # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20]
942
-                    log().debug("We recived a zip file unpacking now ...")
943
-                    from io import BytesIO
944
-                    myzipfile = zipfile.ZipFile(BytesIO(resp.content))
945
-                    return myzipfile.read('%s.xml' % language)
946
-                except zipfile.BadZipfile:
947
-                    self.session.cache.delete_url(url)
948
-                    raise tvdb_error("Bad zip file received from thetvdb.com, could not read it")
949
-            return resp.content
950
-
951
-        else:
952
-            global lastTimeout
953
+    def _loadUrl(self, url, data=None, recache=False, language=None):
954
+        """Return response from The TVDB API"""
955
+
956
+        if not language:
957
+            language = self.config['language']
958
+        if language not in self.config['valid_languages']:
959
+            raise ValueError("Invalid language %s, options are: %s" % (
960
+                language, self.config['valid_languages']
961
+            ))
962
+        self.headers['Accept-Language'] = language
963
+
964
+        # TODO: обрабатывать исключения (Handle Exceptions)
965
+        # TODO: обновлять токен (Update Token)
966
+        # encoded url is used for hashing in the cache so
967
+        # python 2 and 3 generate the same hash
968
+        if not self.__authorized:
969
+            # only authorize of we haven't before and we
970
+            # don't have the url in the cache
971
+            fake_session_for_key = requests.Session()
972
+            fake_session_for_key.headers['Accept-Language'] = language
973
+            cache_key = None
974
             try:
975
-                log().debug("Retrieving URL %s" % url)
976
-                resp = self.urlopener.open(url)
977
-                if 'x-local-cache' in resp.headers:
978
-                    log().debug("URL %s was cached in %s" % (
979
-                        url,
980
-                        resp.headers['x-local-cache'])
981
-                    )
982
-                    if recache:
983
-                        log().debug("Attempting to recache %s" % url)
984
-                        resp.recache()
985
-            except (IOError, urllib2.URLError) as errormsg:
986
-                if not str(errormsg).startswith('HTTP Error'):
987
-                    lastTimeout = datetime.datetime.now()
988
-                raise tvdb_error("Could not connect to server: %s" % (errormsg))
989
-
990
-
991
-            # handle gzipped content,
992
-            # http://dbr.lighthouseapp.com/projects/13342/tickets/72-gzipped-data-patch
993
-            if 'gzip' in resp.headers.get("Content-Encoding", ''):
994
-                if gzip:
995
-                    from StringIO import StringIO
996
-                    stream = StringIO(resp.read())
997
-                    gz = gzip.GzipFile(fileobj=stream)
998
-                    return gz.read()
999
-
1000
-                raise tvdb_error("Received gzip data from thetvdb.com, but could not correctly handle it")
1001
-
1002
-            if 'application/zip' in resp.headers.get("Content-Type", ''):
1003
-                try:
1004
-                    # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20]
1005
-                    log().debug("We recived a zip file unpacking now ...")
1006
-                    from StringIO import StringIO
1007
-                    zipdata = StringIO()
1008
-                    zipdata.write(resp.read())
1009
-                    myzipfile = zipfile.ZipFile(zipdata)
1010
-                    return myzipfile.read('%s.xml' % language)
1011
-                except zipfile.BadZipfile:
1012
-                    if 'x-local-cache' in resp.headers:
1013
-                        resp.delete_cache()
1014
-                    raise tvdb_error("Bad zip file received from thetvdb.com, could not read it")
1015
-
1016
-            return resp.read()
1017
+                # in case the session class has no cache object, fail gracefully
1018
+                cache_key = self.session.cache.create_key(fake_session_for_key.prepare_request(requests.Request('GET', url)))
1019
+            except:
1020
+                pass
1021
+            if not cache_key or not self.session.cache.has_key(cache_key):
1022
+                self.authorize()
1023
+
1024
+        response = self.session.get(url, headers=self.headers)
1025
+        r = response.json()
1026
+        log().debug("loadurl: %s lid=%s" % (url, language))
1027
+        log().debug("response:")
1028
+        log().debug(r)
1029
+        error = r.get('Error')
1030
+        errors = r.get('errors')
1031
+        r_data = r.get('data')
1032
+        links = r.get('links')
1033
+
1034
+        if error:
1035
+            if error == u'Resource not found':
1036
+                # raise(tvdb_resourcenotfound)
1037
+                # handle no data at a different level so it is more specific
1038
+                pass
1039
+            if error == u'Not Authorized':
1040
+                raise(tvdb_notauthorized)
1041
+        if errors:
1042
+            if u'invalidLanguage' in errors:
1043
+                # raise(tvdb_invalidlanguage(errors[u'invalidLanguage']))
1044
+                # invalidLanguage does not mean there is no data
1045
+                # there is just less data
1046
+                pass
1047
+
1048
+        if data and isinstance(data, list):
1049
+            data.extend(r_data)
1050
+        else:
1051
+            data = r_data
1052
+
1053
+        if links and links['next']:
1054
+            url = url.split('?')[0]
1055
+            _url = url + "?page=%s" % links['next']
1056
+            self._loadUrl(_url, data)
1057
+
1058
+        return data
1059
+
1060
+    def authorize(self):
1061
+        log().debug("auth")
1062
+        r = self.session.post('https://api.thetvdb.com/login', json=self.config['auth_payload'], headers=self.headers)
1063
+        r_json = r.json()
1064
+        error = r_json.get('Error')
1065
+        if error:
1066
+            if error == u'Not Authorized':
1067
+                raise(tvdb_notauthorized)
1068
+        token = r_json.get('token')
1069
+        self.headers['Authorization'] = "Bearer %s" % text_type(token)
1070
+        self.__authorized = True
1071
 
1072
     def _getetsrc(self, url, language=None):
1073
         """Loads a URL using caching, returns an ElementTree of the source
1074
         """
1075
         src = self._loadUrl(url, language=language)
1076
 
1077
-
1078
-        # TVDB doesn't sanitize \r (CR) from user input in some fields,
1079
-        # remove it to avoid errors. Change from SickBeard, from will14m
1080
-        if not IS_PY2:
1081
-            # Remove trailing \r byte
1082
-            src = src.replace(b"\r", b"")
1083
-        else:
1084
-            src = src.rstrip("\r") # FIXME: this seems wrong
1085
-
1086
-        try:
1087
-            return ElementTree.fromstring(src)
1088
-        except SyntaxError:
1089
-            src = self._loadUrl(url, recache=True, language=language)
1090
-            try:
1091
-                return ElementTree.fromstring(src)
1092
-            except SyntaxError as exceptionmsg:
1093
-                errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % (
1094
-                    exceptionmsg
1095
-                )
1096
-
1097
-                if self.config['cache_enabled']:
1098
-                    errormsg += "\nFirst try emptying the cache folder at..\n%s" % (
1099
-                        self.config['cache_location']
1100
-                    )
1101
-
1102
-                errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on"
1103
-                errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n"
1104
-                raise tvdb_error(errormsg)
1105
+        return src
1106
 
1107
     def _setItem(self, sid, seas, ep, attrib, value):
1108
         """Creates a new episode, creating Show(), Season() and
1109
@@ -667,9 +922,9 @@
1110
         if sid not in self.shows:
1111
             self.shows[sid] = Show()
1112
         if seas not in self.shows[sid]:
1113
-            self.shows[sid][seas] = Season(show = self.shows[sid])
1114
+            self.shows[sid][seas] = Season(show=self.shows[sid])
1115
         if ep not in self.shows[sid][seas]:
1116
-            self.shows[sid][seas][ep] = Episode(season = self.shows[sid][seas])
1117
+            self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas])
1118
         self.shows[sid][seas][ep][attrib] = value
1119
 
1120
     def _setShowData(self, sid, key, value):
1121
@@ -679,17 +934,6 @@
1122
             self.shows[sid] = Show()
1123
         self.shows[sid].data[key] = value
1124
 
1125
-    def _cleanData(self, data):
1126
-        """Cleans up strings returned by TheTVDB.com
1127
-
1128
-        Issues corrected:
1129
-        - Replaces &amp; with &
1130
-        - Trailing whitespace
1131
-        """
1132
-        data = data.replace(u"&amp;", u"&")
1133
-        data = data.strip()
1134
-        return data
1135
-
1136
     def search(self, series):
1137
         """This searches TheTVDB.com for the series name
1138
         and returns the result list
1139
@@ -697,16 +941,17 @@
1140
         series = url_quote(series.encode("utf-8"))
1141
         log().debug("Searching for show %s" % series)
1142
         seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
1143
+        if not seriesEt:
1144
+            log().debug('Series result returned zero')
1145
+            raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
1146
+
1147
         allSeries = []
1148
         for series in seriesEt:
1149
-            result = dict((k.tag.lower(), k.text) for k in series.getchildren())
1150
-            result['id'] = int(result['id'])
1151
-            result['lid'] = self.config['langabbv_to_id'][result['language']]
1152
-            if 'aliasnames' in result:
1153
-                result['aliasnames'] = result['aliasnames'].split("|")
1154
-            log().debug('Found series %(seriesname)s' % result)
1155
-            allSeries.append(result)
1156
-        
1157
+            series['lid'] = self.config['langabbv_to_id'][self.config['language']]
1158
+            series['language'] = self.config['language']
1159
+            log().debug('Found series %(seriesName)s' % series)
1160
+            allSeries.append(series)
1161
+
1162
         return allSeries
1163
 
1164
     def _getSeries(self, series):
1165
@@ -717,20 +962,16 @@
1166
         """
1167
         allSeries = self.search(series)
1168
 
1169
-        if len(allSeries) == 0:
1170
-            log().debug('Series result returned zero')
1171
-            raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
1172
-
1173
         if self.config['custom_ui'] is not None:
1174
             log().debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
1175
-            ui = self.config['custom_ui'](config = self.config)
1176
+            ui = self.config['custom_ui'](config=self.config)
1177
         else:
1178
             if not self.config['interactive']:
1179
                 log().debug('Auto-selecting first search result using BaseUI')
1180
-                ui = BaseUI(config = self.config)
1181
+                ui = BaseUI(config=self.config)
1182
             else:
1183
                 log().debug('Interactively selecting show using ConsoleUI')
1184
-                ui = ConsoleUI(config = self.config)
1185
+                ui = ConsoleUI(config=self.config)
1186
 
1187
         return ui.selectSeries(allSeries)
1188
 
1189
@@ -742,8 +983,8 @@
1190
 
1191
         >>> t = Tvdb(banners = True)
1192
         >>> t['scrubs']['_banners'].keys()
1193
-        ['fanart', 'poster', 'series', 'season']
1194
-        >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
1195
+        [u'fanart', u'poster', u'seasonwide', u'season', u'series']
1196
+        >>> t['scrubs']['_banners']['poster']['680x1000'][35308]['_bannerpath']
1197
         u'http://thetvdb.com/banners/posters/76156-2.jpg'
1198
         >>>
1199
 
1200
@@ -753,38 +994,37 @@
1201
         This interface will be improved in future versions.
1202
         """
1203
         log().debug('Getting season banners for %s' % (sid))
1204
-        bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
1205
+        bannersEt = self._getetsrc(self.config['url_seriesBanner'] % sid)
1206
         banners = {}
1207
-        for cur_banner in bannersEt.findall('Banner'):
1208
-            bid = cur_banner.find('id').text
1209
-            btype = cur_banner.find('BannerType')
1210
-            btype2 = cur_banner.find('BannerType2')
1211
-            if btype is None or btype2 is None:
1212
-                continue
1213
-            btype, btype2 = btype.text, btype2.text
1214
-            if not btype in banners:
1215
-                banners[btype] = {}
1216
-            if not btype2 in banners[btype]:
1217
-                banners[btype][btype2] = {}
1218
-            if not bid in banners[btype][btype2]:
1219
-                banners[btype][btype2][bid] = {}
1220
-
1221
-            for cur_element in cur_banner.getchildren():
1222
-                tag = cur_element.tag.lower()
1223
-                value = cur_element.text
1224
-                if tag is None or value is None:
1225
+        for cur_banner in bannersEt.keys():
1226
+            banners_info = self._getetsrc(self.config['url_seriesBannerInfo'] % (sid, cur_banner))
1227
+            for banner_info in banners_info:
1228
+                bid = banner_info.get('id')
1229
+                btype = banner_info.get('keyType')
1230
+                btype2 = banner_info.get('resolution')
1231
+                if btype is None or btype2 is None:
1232
                     continue
1233
-                tag, value = tag.lower(), value.lower()
1234
-                banners[btype][btype2][bid][tag] = value
1235
 
1236
-            for k, v in list(banners[btype][btype2][bid].items()):
1237
-                if k.endswith("path"):
1238
-                    new_key = "_%s" % (k)
1239
-                    log().debug("Transforming %s to %s" % (k, new_key))
1240
-                    new_url = self.config['url_artworkPrefix'] % (v)
1241
-                    banners[btype][btype2][bid][new_key] = new_url
1242
+                if btype not in banners:
1243
+                    banners[btype] = {}
1244
+                if btype2 not in banners[btype]:
1245
+                    banners[btype][btype2] = {}
1246
+                if bid not in banners[btype][btype2]:
1247
+                    banners[btype][btype2][bid] = {}
1248
+
1249
+                banners[btype][btype2][bid]['bannerpath'] = banner_info['fileName']
1250
+                banners[btype][btype2][bid]['resolution'] = banner_info['resolution']
1251
+                banners[btype][btype2][bid]['subKey'] = banner_info['subKey']
1252
 
1253
-        self._setShowData(sid, "_banners", banners)
1254
+                for k, v in list(banners[btype][btype2][bid].items()):
1255
+                    if k.endswith("path"):
1256
+                        new_key = "_%s" % k
1257
+                        log().debug("Transforming %s to %s" % (k, new_key))
1258
+                        new_url = self.config['url_artworkPrefix'] % v
1259
+                        banners[btype][btype2][bid][new_key] = new_url
1260
+
1261
+            banners[btype]['raw'] = banners_info
1262
+            self._setShowData(sid, "_banners", banners)
1263
 
1264
     def _parseActors(self, sid):
1265
         """Parsers actors XML, from
1266
@@ -799,13 +1039,13 @@
1267
         >>> type(actors[0])
1268
         <class 'tvdb_api.Actor'>
1269
         >>> actors[0]
1270
-        <Actor "Zach Braff">
1271
+        <Actor u'John C. McGinley'>
1272
         >>> sorted(actors[0].keys())
1273
-        ['id', 'image', 'name', 'role', 'sortorder']
1274
+        [u'id', u'image', u'imageAdded', u'imageAuthor', u'lastUpdated', u'name', u'role', u'seriesId', u'sortOrder']
1275
         >>> actors[0]['name']
1276
-        u'Zach Braff'
1277
+        u'John C. McGinley'
1278
         >>> actors[0]['image']
1279
-        u'http://thetvdb.com/banners/actors/43640.jpg'
1280
+        u'http://thetvdb.com/banners/actors/43638.jpg'
1281
 
1282
         Any key starting with an underscore has been processed (not the raw
1283
         data from the XML)
1284
@@ -814,16 +1054,14 @@
1285
         actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
1286
 
1287
         cur_actors = Actors()
1288
-        for curActorItem in actorsEt.findall("Actor"):
1289
+        for curActorItem in actorsEt:
1290
             curActor = Actor()
1291
-            for curInfo in curActorItem:
1292
-                tag = curInfo.tag.lower()
1293
-                value = curInfo.text
1294
+            for curInfo in curActorItem.keys():
1295
+                tag = curInfo
1296
+                value = curActorItem[curInfo]
1297
                 if value is not None:
1298
                     if tag == "image":
1299
                         value = self.config['url_artworkPrefix'] % (value)
1300
-                    else:
1301
-                        value = self._cleanData(value)
1302
                 curActor[tag] = value
1303
             cur_actors.append(curActor)
1304
         self._setShowData(sid, '_actors', cur_actors)
1305
@@ -838,7 +1076,6 @@
1306
             log().debug('Config language is none, using show language')
1307
             if language is None:
1308
                 raise tvdb_error("config['language'] was None, this should not happen")
1309
-            getShowInLanguage = language
1310
         else:
1311
             log().debug(
1312
                 'Configured language %s override show language of %s' % (
1313
@@ -846,24 +1083,23 @@
1314
                     language
1315
                 )
1316
             )
1317
-            getShowInLanguage = self.config['language']
1318
 
1319
         # Parse show information
1320
         log().debug('Getting all series data for %s' % (sid))
1321
         seriesInfoEt = self._getetsrc(
1322
-            self.config['url_seriesInfo'] % (sid, getShowInLanguage)
1323
+            self.config['url_seriesInfo'] % sid
1324
         )
1325
-        for curInfo in seriesInfoEt.findall("Series")[0]:
1326
-            tag = curInfo.tag.lower()
1327
-            value = curInfo.text
1328
+        for curInfo in seriesInfoEt.keys():
1329
+            tag = curInfo
1330
+            value = seriesInfoEt[curInfo]
1331
 
1332
             if value is not None:
1333
                 if tag in ['banner', 'fanart', 'poster']:
1334
                     value = self.config['url_artworkPrefix'] % (value)
1335
-                else:
1336
-                    value = self._cleanData(value)
1337
 
1338
             self._setShowData(sid, tag, value)
1339
+        # set language
1340
+        self._setShowData(sid, u'language', self.config['language'])
1341
 
1342
         # Parse banners
1343
         if self.config['banners_enabled']:
1344
@@ -876,47 +1112,42 @@
1345
         # Parse episode data
1346
         log().debug('Getting all episodes of %s' % (sid))
1347
 
1348
-        if self.config['useZip']:
1349
-            url = self.config['url_epInfo_zip'] % (sid, language)
1350
-        else:
1351
-            url = self.config['url_epInfo'] % (sid, language)
1352
+        url = self.config['url_epInfo'] % sid
1353
 
1354
-        epsEt = self._getetsrc( url, language=language)
1355
+        epsEt = self._getetsrc(url, language=language)
1356
 
1357
-        for cur_ep in epsEt.findall("Episode"):
1358
+        for cur_ep in epsEt:
1359
 
1360
             if self.config['dvdorder']:
1361
                 log().debug('Using DVD ordering.')
1362
-                use_dvd = cur_ep.find('DVD_season').text != None and cur_ep.find('DVD_episodenumber').text != None
1363
+                use_dvd = cur_ep.get('dvdSeason') is not None and cur_ep.get('dvdEpisodeNumber') is not None
1364
             else:
1365
                 use_dvd = False
1366
 
1367
             if use_dvd:
1368
-                elem_seasnum, elem_epno = cur_ep.find('DVD_season'), cur_ep.find('DVD_episodenumber')
1369
+                elem_seasnum, elem_epno = cur_ep.get('dvdSeason'), cur_ep.get('dvdEpisodeNumber')
1370
             else:
1371
-                elem_seasnum, elem_epno = cur_ep.find('SeasonNumber'), cur_ep.find('EpisodeNumber')
1372
+                elem_seasnum, elem_epno = cur_ep['airedSeason'], cur_ep['airedEpisodeNumber']
1373
 
1374
             if elem_seasnum is None or elem_epno is None:
1375
                 log().warning("An episode has incomplete season/episode number (season: %r, episode: %r)" % (
1376
                     elem_seasnum, elem_epno))
1377
-                log().debug(
1378
-                    " ".join(
1379
-                        "%r is %r" % (child.tag, child.text) for child in cur_ep.getchildren()))
1380
+                #log().debug(
1381
+                #    " ".join(
1382
+                #         "%r is %r" % (child.tag, child.text) for child in cur_ep.getchildren()))
1383
                 # TODO: Should this happen?
1384
-                continue # Skip to next episode
1385
+                continue  # Skip to next episode
1386
 
1387
             # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data
1388
-            seas_no = int(float(elem_seasnum.text))
1389
-            ep_no = int(float(elem_epno.text))
1390
+            seas_no = elem_seasnum
1391
+            ep_no = elem_epno
1392
 
1393
-            for cur_item in cur_ep.getchildren():
1394
-                tag = cur_item.tag.lower()
1395
-                value = cur_item.text
1396
+            for cur_item in cur_ep.keys():
1397
+                tag = cur_item
1398
+                value = cur_ep[cur_item]
1399
                 if value is not None:
1400
                     if tag == 'filename':
1401
                         value = self.config['url_artworkPrefix'] % (value)
1402
-                    else:
1403
-                        value = self._cleanData(value)
1404
                 self._setItem(sid, seas_no, ep_no, tag, value)
1405
 
1406
     def _nameToSid(self, name):
1407
@@ -925,16 +1156,16 @@
1408
         the correct SID.
1409
         """
1410
         if name in self.corrections:
1411
-            log().debug('Correcting %s to %s' % (name, self.corrections[name]) )
1412
+            log().debug('Correcting %s to %s' % (name, self.corrections[name]))
1413
             sid = self.corrections[name]
1414
         else:
1415
-            log().debug('Getting show %s' % (name))
1416
-            selected_series = self._getSeries( name )
1417
-            sname, sid = selected_series['seriesname'], selected_series['id']
1418
-            log().debug('Got %(seriesname)s, id %(id)s' % selected_series)
1419
+            log().debug('Getting show %s' % name)
1420
+            selected_series = self._getSeries(name)
1421
+            sid = selected_series['id']
1422
+            log().debug('Got %(seriesName)s, id %(id)s' % selected_series)
1423
 
1424
             self.corrections[name] = sid
1425
-            self._getShowData(selected_series['id'], selected_series['language'])
1426
+            self._getShowData(selected_series['id'], self.config['language'])
1427
 
1428
         return sid
1429
 
1430
@@ -947,14 +1178,13 @@
1431
             if key not in self.shows:
1432
                 self._getShowData(key, self.config['language'])
1433
             return self.shows[key]
1434
-        
1435
-        key = key.lower() # make key lower case
1436
+
1437
         sid = self._nameToSid(key)
1438
-        log().debug('Got series id %s' % (sid))
1439
+        log().debug('Got series id %s' % sid)
1440
         return self.shows[sid]
1441
 
1442
     def __repr__(self):
1443
-        return str(self.shows)
1444
+        return repr(self.shows)
1445
 
1446
 
1447
 def main():
1448
@@ -964,9 +1194,10 @@
1449
     import logging
1450
     logging.basicConfig(level=logging.DEBUG)
1451
 
1452
-    tvdb_instance = Tvdb(interactive=True, cache=False)
1453
+    tvdb_instance = Tvdb(interactive=False, cache=False)
1454
     print(tvdb_instance['Lost']['seriesname'])
1455
     print(tvdb_instance['Lost'][1][4]['episodename'])
1456
 
1457
+
1458
 if __name__ == '__main__':
1459
     main()
1460
tvdb_api-1.10.tar.gz/tvdb_exceptions.py -> tvdb_api-2.0.tar.gz/tvdb_exceptions.py Changed
59
 
1
@@ -9,44 +9,20 @@
2
 """
3
 
4
 __author__ = "dbr/Ben"
5
-__version__ = "1.10"
6
+__version__ = "2.0"
7
 
8
-__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_shownotfound",
9
-"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"]
10
+import logging
11
 
12
-class tvdb_exception(Exception):
13
-    """Any exception generated by tvdb_api
14
-    """
15
-    pass
16
+__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_notauthorized", "tvdb_shownotfound",
17
+"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound",
18
+"tvdb_resourcenotfound", "tvdb_invalidlanguage"]
19
 
20
-class tvdb_error(tvdb_exception):
21
-    """An error with thetvdb.com (Cannot connect, for example)
22
-    """
23
-    pass
24
+logging.getLogger(__name__).warning(
25
+    "tvdb_exceptions module is deprecated - use classes directly from tvdb_api instead")
26
 
27
-class tvdb_userabort(tvdb_exception):
28
-    """User aborted the interactive selection (via
29
-    the q command, ^c etc)
30
-    """
31
-    pass
32
-
33
-class tvdb_shownotfound(tvdb_exception):
34
-    """Show cannot be found on thetvdb.com (non-existant show)
35
-    """
36
-    pass
37
-
38
-class tvdb_seasonnotfound(tvdb_exception):
39
-    """Season cannot be found on thetvdb.com
40
-    """
41
-    pass
42
-
43
-class tvdb_episodenotfound(tvdb_exception):
44
-    """Episode cannot be found on thetvdb.com
45
-    """
46
-    pass
47
-
48
-class tvdb_attributenotfound(tvdb_exception):
49
-    """Raised if an episode does not have the requested
50
-    attribute (such as a episode name)
51
-    """
52
-    pass
53
+from tvdb_api import (
54
+    tvdb_error, tvdb_userabort, tvdb_notauthorized, tvdb_shownotfound,
55
+    tvdb_seasonnotfound, tvdb_episodenotfound,
56
+    tvdb_resourcenotfound, tvdb_invalidlanguage,
57
+    tvdb_attributenotfound
58
+)
59
tvdb_api-1.10.tar.gz/tvdb_ui.py -> tvdb_api-2.0.tar.gz/tvdb_ui.py Changed
169
 
1
@@ -5,45 +5,9 @@
2
 #repository:http://github.com/dbr/tvdb_api
3
 #license:unlicense (http://unlicense.org/)
4
 
5
-"""Contains included user interfaces for Tvdb show selection.
6
-
7
-A UI is a callback. A class, it's __init__ function takes two arguments:
8
-
9
-- config, which is the Tvdb config dict, setup in tvdb_api.py
10
-- log, which is Tvdb's logger instance (which uses the logging module). You can
11
-call log.info() log.warning() etc
12
-
13
-It must have a method "selectSeries", this is passed a list of dicts, each dict
14
-contains the the keys "name" (human readable show name), and "sid" (the shows
15
-ID as on thetvdb.com). For example:
16
-
17
-[{'name': u'Lost', 'sid': u'73739'},
18
- {'name': u'Lost Universe', 'sid': u'73181'}]
19
-
20
-The "selectSeries" method must return the appropriate dict, or it can raise
21
-tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show
22
-cannot be found).
23
-
24
-A simple example callback, which returns a random series:
25
-
26
->>> import random
27
->>> from tvdb_ui import BaseUI
28
->>> class RandomUI(BaseUI):
29
-...    def selectSeries(self, allSeries):
30
-...            import random
31
-...            return random.choice(allSeries)
32
-
33
-Then to use it..
34
-
35
->>> from tvdb_api import Tvdb
36
->>> t = Tvdb(custom_ui = RandomUI)
37
->>> random_matching_series = t['Lost']
38
->>> type(random_matching_series)
39
-<class 'tvdb_api.Show'>
40
-"""
41
 
42
 __author__ = "dbr/Ben"
43
-__version__ = "1.10"
44
+__version__ = "2.0"
45
 
46
 import sys
47
 import logging
48
@@ -51,117 +15,7 @@
49
 
50
 from tvdb_exceptions import tvdb_userabort
51
 
52
+logging.getLogger(__name__).warning(
53
+    "tvdb_ui module is deprecated - use classes directly from tvdb_api instead")
54
 
55
-IS_PY2 = sys.version_info[0] == 2
56
-
57
-if IS_PY2:
58
-    user_input = raw_input
59
-else:
60
-    user_input = input
61
-
62
-
63
-def log():
64
-    return logging.getLogger(__name__)
65
-
66
-class BaseUI:
67
-    """Default non-interactive UI, which auto-selects first results
68
-    """
69
-    def __init__(self, config, log = None):
70
-        self.config = config
71
-        if log is not None:
72
-            warnings.warn("the UI's log parameter is deprecated, instead use\n"
73
-                "use import logging; logging.getLogger('ui').info('blah')\n"
74
-                "The self.log attribute will be removed in the next version")
75
-            self.log = logging.getLogger(__name__)
76
-
77
-    def selectSeries(self, allSeries):
78
-        return allSeries[0]
79
-
80
-
81
-class ConsoleUI(BaseUI):
82
-    """Interactively allows the user to select a show from a console based UI
83
-    """
84
-
85
-    def _displaySeries(self, allSeries, limit = 6):
86
-        """Helper function, lists series with corresponding ID
87
-        """
88
-        if limit is not None:
89
-            toshow = allSeries[:limit]
90
-        else:
91
-            toshow = allSeries
92
-
93
-        print("TVDB Search Results:")
94
-        for i, cshow in enumerate(toshow):
95
-            i_show = i + 1 # Start at more human readable number 1 (not 0)
96
-            log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesname']))
97
-            if i == 0:
98
-                extra = " (default)"
99
-            else:
100
-                extra = ""
101
-
102
-            output = "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % (
103
-                i_show,
104
-                cshow['seriesname'],
105
-                cshow['language'],
106
-                str(cshow['id']),
107
-                cshow['lid'],
108
-                extra
109
-            )
110
-            if IS_PY2:
111
-                print(output.encode("UTF-8", "ignore"))
112
-            else:
113
-                print(output)
114
-
115
-    def selectSeries(self, allSeries):
116
-        self._displaySeries(allSeries)
117
-
118
-        if len(allSeries) == 1:
119
-            # Single result, return it!
120
-            print("Automatically selecting only result")
121
-            return allSeries[0]
122
-
123
-        if self.config['select_first'] is True:
124
-            print("Automatically returning first search result")
125
-            return allSeries[0]
126
-
127
-        while True: # return breaks this loop
128
-            try:
129
-                print("Enter choice (first number, return for default, 'all', ? for help):")
130
-                ans = user_input()
131
-            except KeyboardInterrupt:
132
-                raise tvdb_userabort("User aborted (^c keyboard interupt)")
133
-            except EOFError:
134
-                raise tvdb_userabort("User aborted (EOF received)")
135
-
136
-            log().debug('Got choice of: %s' % (ans))
137
-            try:
138
-                selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
139
-            except ValueError: # Input was not number
140
-                if len(ans.strip()) == 0:
141
-                    # Default option
142
-                    log().debug('Default option, returning first series')
143
-                    return allSeries[0]
144
-                if ans == "q":
145
-                    log().debug('Got quit command (q)')
146
-                    raise tvdb_userabort("User aborted ('q' quit command)")
147
-                elif ans == "?":
148
-                    print("## Help")
149
-                    print("# Enter the number that corresponds to the correct show.")
150
-                    print("# a - display all results")
151
-                    print("# all - display all results")
152
-                    print("# ? - this help")
153
-                    print("# q - abort tvnamer")
154
-                    print("# Press return with no input to select first result")
155
-                elif ans.lower() in ["a", "all"]:
156
-                    self._displaySeries(allSeries, limit = None)
157
-                else:
158
-                    log().debug('Unknown keypress %s' % (ans))
159
-            else:
160
-                log().debug('Trying to return ID: %d' % (selected_id))
161
-                try:
162
-                    return allSeries[selected_id]
163
-                except IndexError:
164
-                    log().debug('Invalid show number entered!')
165
-                    print("Invalid number (%s) selected!")
166
-                    self._displaySeries(allSeries)
167
-
168
+from tvdb_api import BaseUI, ConsoleUI
169
Refresh

No build results available

Refresh

No rpmlint results available

Request History
Aliaksei Padvalski's avatar

awissu created request over 6 years ago

- Update to 2.0:
* Switch to TheTVDB new JSON based API - issue #57
- Add support for Python3
- Fix: non-executable-script
- Update to description
- Spec file cleanup


Olaf Hering's avatar

olh accepted request over 6 years ago