imdbpie 5.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- imdbpie/__init__.py +4 -0
- imdbpie/auth.py +99 -0
- imdbpie/constants.py +8 -0
- imdbpie/exceptions.py +3 -0
- imdbpie/facade.py +430 -0
- imdbpie/imdbpie.py +430 -0
- imdbpie/objects.py +162 -0
- imdbpie-5.8.0.dist-info/METADATA +93 -0
- imdbpie-5.8.0.dist-info/RECORD +12 -0
- imdbpie-5.8.0.dist-info/WHEEL +5 -0
- imdbpie-5.8.0.dist-info/licenses/LICENSE +13 -0
- imdbpie-5.8.0.dist-info/top_level.txt +1 -0
imdbpie/__init__.py
ADDED
imdbpie/auth.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import, unicode_literals
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import tempfile
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from urllib.parse import urlparse, parse_qs
|
|
8
|
+
import diskcache_community
|
|
9
|
+
import requests
|
|
10
|
+
from botocore.auth import SigV4Auth
|
|
11
|
+
from botocore.awsrequest import AWSRequest
|
|
12
|
+
from botocore.credentials import Credentials
|
|
13
|
+
from dateutil.parser import parse
|
|
14
|
+
from dateutil.tz import tzutc
|
|
15
|
+
|
|
16
|
+
from .constants import APP_KEY, HOST, USER_AGENT, BASE_URI
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_credentials():
|
|
20
|
+
url = '{0}/authentication/credentials/temporary/ios82'.format(BASE_URI)
|
|
21
|
+
response = requests.post(
|
|
22
|
+
url, json={'appKey': APP_KEY}, headers={'User-Agent': USER_AGENT}
|
|
23
|
+
)
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
return json.loads(response.content.decode('utf8'))['resource']
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Auth(object):
|
|
29
|
+
SOON_EXPIRES_SECONDS = 60
|
|
30
|
+
_CREDS_STORAGE_KEY = 'imdbpie-credentials'
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self._cachedir = tempfile.gettempdir()
|
|
34
|
+
|
|
35
|
+
def _cache(self):
|
|
36
|
+
return diskcache_community.Cache(
|
|
37
|
+
directory=self._cachedir,
|
|
38
|
+
disk=diskcache_community.SafeDisk.with_options(on_error='miss'),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def _get_creds(self):
|
|
42
|
+
with self._cache() as cache:
|
|
43
|
+
return cache.get(self._CREDS_STORAGE_KEY)
|
|
44
|
+
|
|
45
|
+
def _set_creds(self, creds):
|
|
46
|
+
with self._cache() as cache:
|
|
47
|
+
cache[self._CREDS_STORAGE_KEY] = creds
|
|
48
|
+
return creds
|
|
49
|
+
|
|
50
|
+
def clear_cached_credentials(self):
|
|
51
|
+
with self._cache() as cache:
|
|
52
|
+
cache.delete(self._CREDS_STORAGE_KEY)
|
|
53
|
+
|
|
54
|
+
def _creds_soon_expiring(self):
|
|
55
|
+
creds = self._get_creds()
|
|
56
|
+
if not creds:
|
|
57
|
+
return creds, True
|
|
58
|
+
expires_at = parse(creds['expirationTimeStamp'])
|
|
59
|
+
now = datetime.now(tzutc())
|
|
60
|
+
if now < expires_at:
|
|
61
|
+
time_diff = expires_at - now
|
|
62
|
+
if time_diff.total_seconds() < self.SOON_EXPIRES_SECONDS:
|
|
63
|
+
# creds will soon expire, so renew them
|
|
64
|
+
return creds, True
|
|
65
|
+
return creds, False
|
|
66
|
+
else:
|
|
67
|
+
return creds, True
|
|
68
|
+
|
|
69
|
+
def get_auth_headers(self, url_path):
|
|
70
|
+
creds, soon_expires = self._creds_soon_expiring()
|
|
71
|
+
if soon_expires:
|
|
72
|
+
creds = self._set_creds(creds=_get_credentials())
|
|
73
|
+
credentials = Credentials(
|
|
74
|
+
access_key=creds['accessKeyId'],
|
|
75
|
+
secret_key=creds['secretAccessKey'],
|
|
76
|
+
token=creds['sessionToken'],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
parsed_url = urlparse(url_path)
|
|
80
|
+
params = {
|
|
81
|
+
key: val[0] for key, val in parse_qs(parsed_url.query).items()
|
|
82
|
+
}
|
|
83
|
+
req = requests.Request(
|
|
84
|
+
'GET',
|
|
85
|
+
f'https://{HOST}{parsed_url.path}',
|
|
86
|
+
params=params,
|
|
87
|
+
data='',
|
|
88
|
+
headers={},
|
|
89
|
+
)
|
|
90
|
+
prepared_request = req.prepare()
|
|
91
|
+
prepared_request.headers['User-Agent'] = USER_AGENT
|
|
92
|
+
aws_request = AWSRequest(
|
|
93
|
+
method=prepared_request.method,
|
|
94
|
+
url=prepared_request.url,
|
|
95
|
+
data=prepared_request.body,
|
|
96
|
+
headers=prepared_request.headers,
|
|
97
|
+
)
|
|
98
|
+
SigV4Auth(credentials, 'imdbapi', 'us-east-1').add_auth(aws_request)
|
|
99
|
+
return aws_request.prepare().headers
|
imdbpie/constants.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import, unicode_literals
|
|
3
|
+
|
|
4
|
+
HOST = 'api.imdbws.com'
|
|
5
|
+
BASE_URI = 'https://{0}'.format(HOST)
|
|
6
|
+
SEARCH_BASE_URI = 'https://v2.sg.media-imdb.com'
|
|
7
|
+
USER_AGENT = 'IMDb/8.3.1 (iPhone9,4; iOS 11.2.1)'
|
|
8
|
+
APP_KEY = '76a6cc20-6073-4290-8a2c-951b4580ae4a'
|
imdbpie/exceptions.py
ADDED
imdbpie/facade.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import, unicode_literals
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from dateutil.parser import parse
|
|
7
|
+
|
|
8
|
+
from .imdbpie import Imdb
|
|
9
|
+
from .objects import (
|
|
10
|
+
Award,
|
|
11
|
+
FilmingLocation,
|
|
12
|
+
Image,
|
|
13
|
+
Name,
|
|
14
|
+
NameSearchResult,
|
|
15
|
+
Title,
|
|
16
|
+
TitleBusiness,
|
|
17
|
+
TitleCertificate,
|
|
18
|
+
TitleEpisodes,
|
|
19
|
+
TitleName,
|
|
20
|
+
TitleRelease,
|
|
21
|
+
TitleSearchResult,
|
|
22
|
+
TitleVideo,
|
|
23
|
+
WatchOption,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
REGEX_IMDB_ID = re.compile(r'([a-zA-Z]{2}[0-9]{7,8})')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ImdbFacade(object):
|
|
30
|
+
def __init__(self, client=None):
|
|
31
|
+
self._client = client or Imdb()
|
|
32
|
+
|
|
33
|
+
def get_title(self, imdb_id):
|
|
34
|
+
title_data, title_aux_data = self._get_title_data(imdb_id=imdb_id)
|
|
35
|
+
try:
|
|
36
|
+
runtime = title_aux_data['runningTimes'][0]['timeMinutes']
|
|
37
|
+
except (KeyError, IndexError):
|
|
38
|
+
runtime = None
|
|
39
|
+
try:
|
|
40
|
+
episodes = TitleEpisodes(facade=self, imdb_id=imdb_id)
|
|
41
|
+
except LookupError:
|
|
42
|
+
episodes = ()
|
|
43
|
+
try:
|
|
44
|
+
season = title_aux_data['season']
|
|
45
|
+
episode = title_aux_data['episode']
|
|
46
|
+
except KeyError:
|
|
47
|
+
season = None
|
|
48
|
+
episode = None
|
|
49
|
+
return Title(
|
|
50
|
+
season=season,
|
|
51
|
+
episode=episode,
|
|
52
|
+
episodes=episodes,
|
|
53
|
+
runtime=runtime,
|
|
54
|
+
**title_data
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def get_name(self, imdb_id):
|
|
58
|
+
name_data = self._client.get_name(imdb_id=imdb_id)
|
|
59
|
+
base = name_data['base']
|
|
60
|
+
name = base['name']
|
|
61
|
+
imdb_id = self._parse_id(base['id'])
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
image_data = base['image']
|
|
65
|
+
image = Image(
|
|
66
|
+
url=image_data['url'],
|
|
67
|
+
height=image_data['height'],
|
|
68
|
+
width=image_data['width'],
|
|
69
|
+
)
|
|
70
|
+
except KeyError:
|
|
71
|
+
image = None
|
|
72
|
+
|
|
73
|
+
gender = (base.get('gender') or '').lower() or None
|
|
74
|
+
birth_date = base.get('birthDate')
|
|
75
|
+
date_of_birth = parse(birth_date).date() if birth_date else None
|
|
76
|
+
birth_place = base.get('birthPlace')
|
|
77
|
+
try:
|
|
78
|
+
bios = tuple(b['text'] for b in base['miniBios'])
|
|
79
|
+
except KeyError:
|
|
80
|
+
bios = ()
|
|
81
|
+
|
|
82
|
+
filmography_data = self._client.get_name_filmography(imdb_id)
|
|
83
|
+
filmography = tuple(
|
|
84
|
+
self._parse_id(f['id']) for f in filmography_data['filmography']
|
|
85
|
+
)
|
|
86
|
+
known_for = tuple(
|
|
87
|
+
self._parse_id(item['id'])
|
|
88
|
+
for item in (name_data.get('knownFor') or ())
|
|
89
|
+
if item.get('id')
|
|
90
|
+
)
|
|
91
|
+
return Name(
|
|
92
|
+
name=name,
|
|
93
|
+
imdb_id=imdb_id,
|
|
94
|
+
date_of_birth=date_of_birth,
|
|
95
|
+
gender=gender,
|
|
96
|
+
birth_place=birth_place,
|
|
97
|
+
bios=bios,
|
|
98
|
+
image=image,
|
|
99
|
+
filmography=filmography,
|
|
100
|
+
jobs=tuple(name_data.get('jobs') or ()),
|
|
101
|
+
known_for=known_for,
|
|
102
|
+
real_name=base.get('realName'),
|
|
103
|
+
height_centimeters=base.get('heightCentimeters'),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def search_for_name(self, query):
|
|
107
|
+
results = []
|
|
108
|
+
for result in self._client.search_for_name(query):
|
|
109
|
+
result = NameSearchResult(
|
|
110
|
+
imdb_id=result['imdb_id'],
|
|
111
|
+
name=result['name'],
|
|
112
|
+
)
|
|
113
|
+
results.append(result)
|
|
114
|
+
return tuple(results)
|
|
115
|
+
|
|
116
|
+
def search_for_title(self, query):
|
|
117
|
+
results = []
|
|
118
|
+
for result in self._client.search_for_title(query):
|
|
119
|
+
if result['year']:
|
|
120
|
+
year = int(result['year'])
|
|
121
|
+
else:
|
|
122
|
+
year = None
|
|
123
|
+
result = TitleSearchResult(
|
|
124
|
+
imdb_id=result['imdb_id'],
|
|
125
|
+
title=result['title'],
|
|
126
|
+
type=result['type'],
|
|
127
|
+
year=year,
|
|
128
|
+
)
|
|
129
|
+
results.append(result)
|
|
130
|
+
return tuple(results)
|
|
131
|
+
|
|
132
|
+
def _get_writers(self, top_crew_data):
|
|
133
|
+
return tuple(
|
|
134
|
+
TitleName(
|
|
135
|
+
name=i['name'],
|
|
136
|
+
job=i.get('job'),
|
|
137
|
+
category=i.get('category'),
|
|
138
|
+
imdb_id=self._parse_id(i['id']),
|
|
139
|
+
)
|
|
140
|
+
for i in top_crew_data['writers']
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _get_stars(self, principals_data):
|
|
144
|
+
return tuple(
|
|
145
|
+
TitleName(
|
|
146
|
+
name=i['name'],
|
|
147
|
+
job=i.get('job'),
|
|
148
|
+
characters=tuple(i.get('characters', ())),
|
|
149
|
+
category=i.get('category'),
|
|
150
|
+
imdb_id=self._parse_id(i['id']),
|
|
151
|
+
)
|
|
152
|
+
for i in principals_data
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _get_creators(self, top_crew_data):
|
|
156
|
+
return tuple(
|
|
157
|
+
TitleName(
|
|
158
|
+
name=i['name'],
|
|
159
|
+
job=i.get('job'),
|
|
160
|
+
category=i.get('category'),
|
|
161
|
+
imdb_id=self._parse_id(i['id']),
|
|
162
|
+
)
|
|
163
|
+
for i in top_crew_data['writers']
|
|
164
|
+
if i.get('job') == 'creator'
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _get_directors(self, top_crew_data):
|
|
168
|
+
return tuple(
|
|
169
|
+
TitleName(
|
|
170
|
+
name=i['name'],
|
|
171
|
+
job=i.get('job'),
|
|
172
|
+
category=i.get('category'),
|
|
173
|
+
imdb_id=self._parse_id(i['id']),
|
|
174
|
+
)
|
|
175
|
+
for i in top_crew_data['directors']
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _get_credits(self, credits_data):
|
|
179
|
+
credits = []
|
|
180
|
+
for category in credits_data.get('credits', ()):
|
|
181
|
+
for item in credits_data['credits'][category]:
|
|
182
|
+
credits.append(
|
|
183
|
+
TitleName(
|
|
184
|
+
name=item['name'],
|
|
185
|
+
category=item.get('category'),
|
|
186
|
+
job=item.get('job'),
|
|
187
|
+
imdb_id=self._parse_id(item['id']),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
return tuple(credits)
|
|
191
|
+
|
|
192
|
+
def _parse_id(self, string):
|
|
193
|
+
match = REGEX_IMDB_ID.search(string or '')
|
|
194
|
+
if not match:
|
|
195
|
+
raise ValueError('invalid imdb id: {0!r}'.format(string))
|
|
196
|
+
return match.group(1)
|
|
197
|
+
|
|
198
|
+
def _get_title_data(self, imdb_id):
|
|
199
|
+
base_title_data = self._client.get_title(imdb_id=imdb_id)
|
|
200
|
+
top_crew_data = self._client.get_title_top_crew(imdb_id=imdb_id)
|
|
201
|
+
title_aux_data = self._client.get_title_auxiliary(imdb_id=imdb_id)
|
|
202
|
+
credits_data = self._client.get_title_credits(imdb_id=imdb_id)
|
|
203
|
+
|
|
204
|
+
title = base_title_data['base']['title']
|
|
205
|
+
year = base_title_data['base'].get('year')
|
|
206
|
+
try:
|
|
207
|
+
rating = float(base_title_data['ratings']['rating'])
|
|
208
|
+
except KeyError:
|
|
209
|
+
rating = None
|
|
210
|
+
type_ = base_title_data['base']['titleType'].lower()
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
releases_data = self._client.get_title_releases(imdb_id=imdb_id)
|
|
214
|
+
except LookupError:
|
|
215
|
+
release_date = None
|
|
216
|
+
releases = ()
|
|
217
|
+
else:
|
|
218
|
+
release_date = parse(releases_data['releases'][0]['date']).date()
|
|
219
|
+
releases = tuple(
|
|
220
|
+
TitleRelease(date=parse(r['date']).date(), region=r['region'])
|
|
221
|
+
for r in releases_data['releases']
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
rating_count = base_title_data['ratings']['ratingCount']
|
|
226
|
+
except KeyError:
|
|
227
|
+
rating_count = 0
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
plot_outline = base_title_data['plot']['outline']['text']
|
|
231
|
+
except KeyError:
|
|
232
|
+
plot_outline = None
|
|
233
|
+
|
|
234
|
+
writers = self._get_writers(top_crew_data)
|
|
235
|
+
directors = self._get_directors(top_crew_data)
|
|
236
|
+
creators = self._get_creators(top_crew_data)
|
|
237
|
+
try:
|
|
238
|
+
genres = tuple(g.lower() for g in title_aux_data.get('genres'))
|
|
239
|
+
except TypeError:
|
|
240
|
+
genres = ()
|
|
241
|
+
credits = self._get_credits(credits_data)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
certification = title_aux_data['certificate']['certificate']
|
|
245
|
+
except (KeyError, TypeError):
|
|
246
|
+
certification = None
|
|
247
|
+
stars = self._get_stars(title_aux_data['principals'])
|
|
248
|
+
try:
|
|
249
|
+
image_data = title_aux_data['image']
|
|
250
|
+
image = Image(
|
|
251
|
+
url=image_data['url'],
|
|
252
|
+
height=image_data['height'],
|
|
253
|
+
width=image_data['width'],
|
|
254
|
+
)
|
|
255
|
+
except KeyError:
|
|
256
|
+
image = None
|
|
257
|
+
metacritic_info = title_aux_data.get('metacriticInfo') or {}
|
|
258
|
+
certificate_data = title_aux_data.get('certificate') or {}
|
|
259
|
+
return (
|
|
260
|
+
dict(
|
|
261
|
+
imdb_id=imdb_id,
|
|
262
|
+
title=title,
|
|
263
|
+
year=year,
|
|
264
|
+
rating=rating,
|
|
265
|
+
type=type_,
|
|
266
|
+
release_date=release_date,
|
|
267
|
+
releases=releases,
|
|
268
|
+
plot_outline=plot_outline,
|
|
269
|
+
rating_count=rating_count,
|
|
270
|
+
writers=writers,
|
|
271
|
+
directors=directors,
|
|
272
|
+
creators=creators,
|
|
273
|
+
genres=genres,
|
|
274
|
+
credits=credits,
|
|
275
|
+
certification=certification,
|
|
276
|
+
image=image,
|
|
277
|
+
stars=stars,
|
|
278
|
+
metacritic=metacritic_info.get('metaScore'),
|
|
279
|
+
awards_summary=title_aux_data.get('awards'),
|
|
280
|
+
spoken_languages=tuple(
|
|
281
|
+
title_aux_data.get('spokenLanguages') or ()
|
|
282
|
+
),
|
|
283
|
+
countries=tuple(title_aux_data.get('origins') or ()),
|
|
284
|
+
alternate_titles=tuple(
|
|
285
|
+
title_aux_data.get('alternateTitlesSample') or ()
|
|
286
|
+
),
|
|
287
|
+
alternate_titles_count=(
|
|
288
|
+
title_aux_data.get('alternateTitlesCount') or 0
|
|
289
|
+
),
|
|
290
|
+
top_rank=title_aux_data.get('topRank'),
|
|
291
|
+
trailer=self._parse_trailer(title_aux_data.get('videos')),
|
|
292
|
+
production_status=title_aux_data.get('productionStatus'),
|
|
293
|
+
certificate_reason=certificate_data.get('ratingReason'),
|
|
294
|
+
),
|
|
295
|
+
title_aux_data,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def _parse_trailer(videos_data):
|
|
300
|
+
trailer = (videos_data or {}).get('mainTrailer')
|
|
301
|
+
if not trailer:
|
|
302
|
+
return None
|
|
303
|
+
url = None
|
|
304
|
+
for encoding in trailer.get('encodings') or ():
|
|
305
|
+
if encoding.get('play'):
|
|
306
|
+
url = encoding['play']
|
|
307
|
+
break
|
|
308
|
+
return TitleVideo(
|
|
309
|
+
content_type=trailer.get('contentType'),
|
|
310
|
+
description=trailer.get('description'),
|
|
311
|
+
url=url,
|
|
312
|
+
duration_seconds=trailer.get('durationSeconds'),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def get_title_watch_options(self, imdb_id):
|
|
316
|
+
data = self._client.get_title_watch_options(imdb_id=imdb_id)
|
|
317
|
+
options = []
|
|
318
|
+
for group in data.get('optionGroups') or ():
|
|
319
|
+
category = (group.get('displayName') or '').lower()
|
|
320
|
+
for option in group.get('watchOptions') or ():
|
|
321
|
+
link = option.get('link') or {}
|
|
322
|
+
provider = option.get('provider') or {}
|
|
323
|
+
options.append(
|
|
324
|
+
WatchOption(
|
|
325
|
+
provider=option.get('primaryText'),
|
|
326
|
+
category=category,
|
|
327
|
+
url=link.get('uri'),
|
|
328
|
+
provider_id=provider.get('providerId'),
|
|
329
|
+
notes=option.get('secondaryText'),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
return tuple(options)
|
|
333
|
+
|
|
334
|
+
def get_title_business(self, imdb_id):
|
|
335
|
+
data = self._client.get_title_business(imdb_id=imdb_id)
|
|
336
|
+
gross = data.get('gross') or {}
|
|
337
|
+
aggregations = gross.get('aggregations') or []
|
|
338
|
+
worldwide_gross = None
|
|
339
|
+
for aggregation in aggregations:
|
|
340
|
+
if aggregation.get('area') == 'World-wide':
|
|
341
|
+
worldwide_gross = aggregation.get('total')
|
|
342
|
+
break
|
|
343
|
+
opening_weekend = data.get('openingWeekendGross') or {}
|
|
344
|
+
return TitleBusiness(
|
|
345
|
+
budget=data.get('budget'),
|
|
346
|
+
worldwide_gross=worldwide_gross,
|
|
347
|
+
gross=tuple(gross.get('regional') or ()),
|
|
348
|
+
opening_weekend_gross=tuple(opening_weekend.get('regional') or ()),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def get_title_keywords(self, imdb_id):
|
|
352
|
+
data = self._client.get_title_keywords(imdb_id=imdb_id)
|
|
353
|
+
return tuple(data.get('keywords') or ())
|
|
354
|
+
|
|
355
|
+
def get_title_certificates(self, imdb_id):
|
|
356
|
+
data = self._client.get_title_certificates(imdb_id=imdb_id)
|
|
357
|
+
certificates = []
|
|
358
|
+
for country, entries in (data.get('certificates') or {}).items():
|
|
359
|
+
for entry in entries or ():
|
|
360
|
+
certificates.append(
|
|
361
|
+
TitleCertificate(
|
|
362
|
+
country=country,
|
|
363
|
+
certificate=entry.get('certificate'),
|
|
364
|
+
attributes=tuple(entry.get('attributes') or ()),
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
return tuple(certificates)
|
|
368
|
+
|
|
369
|
+
def get_title_filming_locations(self, imdb_id):
|
|
370
|
+
data = self._client.get_title_filming_locations(imdb_id=imdb_id)
|
|
371
|
+
return tuple(
|
|
372
|
+
FilmingLocation(
|
|
373
|
+
location=item.get('location'),
|
|
374
|
+
attributes=tuple(item.get('attributes') or ()),
|
|
375
|
+
notes=tuple(item.get('extras') or ()),
|
|
376
|
+
)
|
|
377
|
+
for item in (data.get('locations') or ())
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def get_title_crazy_credits(self, imdb_id):
|
|
381
|
+
data = self._client.get_title_crazy_credits(imdb_id=imdb_id)
|
|
382
|
+
return tuple(
|
|
383
|
+
item['text']
|
|
384
|
+
for item in (data.get('crazycredits') or ())
|
|
385
|
+
if item.get('text')
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def get_title_principals(self, imdb_id):
|
|
389
|
+
data = self._client.get_title_principals(imdb_id=imdb_id)
|
|
390
|
+
return tuple(
|
|
391
|
+
TitleName(
|
|
392
|
+
name=item.get('name'),
|
|
393
|
+
category=item.get('category'),
|
|
394
|
+
imdb_id=(
|
|
395
|
+
self._parse_id(item['id']) if item.get('id') else None
|
|
396
|
+
),
|
|
397
|
+
job=item.get('job'),
|
|
398
|
+
characters=tuple(item.get('characters') or ()),
|
|
399
|
+
)
|
|
400
|
+
for item in (data.get('principals') or ())
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def get_name_known_for(self, imdb_id):
|
|
404
|
+
data = self._client.get_name_known_for(imdb_id=imdb_id)
|
|
405
|
+
return tuple(
|
|
406
|
+
self._parse_id(item['id'])
|
|
407
|
+
for item in (data.get('knownFor') or ())
|
|
408
|
+
if item.get('id')
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def get_name_awards(self, imdb_id):
|
|
412
|
+
data = self._client.get_name_awards(imdb_id=imdb_id)
|
|
413
|
+
awards = []
|
|
414
|
+
for item in data.get('awards') or ():
|
|
415
|
+
titles = (item.get('nominations') or {}).get('titles') or ()
|
|
416
|
+
awards.append(
|
|
417
|
+
Award(
|
|
418
|
+
name=item.get('awardName'),
|
|
419
|
+
category=item.get('category'),
|
|
420
|
+
year=item.get('year'),
|
|
421
|
+
won=bool(item.get('isWinner')),
|
|
422
|
+
event_name=item.get('eventName'),
|
|
423
|
+
title_ids=tuple(
|
|
424
|
+
self._parse_id(title['id'])
|
|
425
|
+
for title in titles
|
|
426
|
+
if title.get('id')
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
return tuple(awards)
|
imdbpie/imdbpie.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import, unicode_literals
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import re
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from datetime import date
|
|
9
|
+
import tempfile
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from text_unidecode import unidecode
|
|
13
|
+
import requests
|
|
14
|
+
from urllib.parse import urlencode, urljoin, quote
|
|
15
|
+
from http import client as httplib
|
|
16
|
+
|
|
17
|
+
from .constants import BASE_URI, SEARCH_BASE_URI
|
|
18
|
+
from .auth import Auth
|
|
19
|
+
from .exceptions import ImdbAPIError
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# client method name -> api path
|
|
24
|
+
_SIMPLE_GET_ENDPOINTS = {
|
|
25
|
+
'get_name_images': '/name/{imdb_id}/images',
|
|
26
|
+
'get_name_videos': '/name/{imdb_id}/videos',
|
|
27
|
+
'get_title_metacritic_reviews': '/title/{imdb_id}/metacritic',
|
|
28
|
+
'get_title_user_reviews': '/title/{imdb_id}/userreviews',
|
|
29
|
+
'get_title_videos': '/title/{imdb_id}/videos',
|
|
30
|
+
'get_title_images': '/title/{imdb_id}/images',
|
|
31
|
+
'get_title_companies': '/title/{imdb_id}/companies',
|
|
32
|
+
'get_title_technical': '/title/{imdb_id}/technical',
|
|
33
|
+
'get_title_trivia': '/title/{imdb_id}/trivia',
|
|
34
|
+
'get_title_goofs': '/title/{imdb_id}/goofs',
|
|
35
|
+
'get_title_soundtracks': '/title/{imdb_id}/soundtracks',
|
|
36
|
+
'get_title_news': '/title/{imdb_id}/news',
|
|
37
|
+
'get_title_plot': '/title/{imdb_id}/plot',
|
|
38
|
+
'get_title_plot_synopsis': '/title/{imdb_id}/plotsynopsis',
|
|
39
|
+
'get_title_plot_taglines': '/title/{imdb_id}/taglines',
|
|
40
|
+
'get_title_versions': '/title/{imdb_id}/versions',
|
|
41
|
+
'get_title_releases': '/title/{imdb_id}/releases',
|
|
42
|
+
'get_title_quotes': '/title/{imdb_id}/quotes',
|
|
43
|
+
'get_title_connections': '/title/{imdb_id}/connections',
|
|
44
|
+
'get_title_genres': '/title/{imdb_id}/genres',
|
|
45
|
+
'get_title_similarities': '/title/{imdb_id}/similarities',
|
|
46
|
+
'get_title_awards': '/title/{imdb_id}/awards',
|
|
47
|
+
'get_title_ratings': '/title/{imdb_id}/ratings',
|
|
48
|
+
'get_title_credits': '/title/{imdb_id}/fullcredits',
|
|
49
|
+
'get_title_keywords': '/title/{imdb_id}/keywords',
|
|
50
|
+
'get_title_certificates': '/title/{imdb_id}/certificates',
|
|
51
|
+
'get_title_business': '/title/{imdb_id}/business',
|
|
52
|
+
'get_title_watch_options': '/title/{imdb_id}/watchoptions',
|
|
53
|
+
'get_title_principals': '/title/{imdb_id}/principals',
|
|
54
|
+
'get_title_filming_locations': '/title/{imdb_id}/filminglocations',
|
|
55
|
+
'get_title_crazy_credits': '/title/{imdb_id}/crazycredits',
|
|
56
|
+
'get_name': '/name/{imdb_id}/fulldetails',
|
|
57
|
+
'get_name_filmography': '/name/{imdb_id}/filmography',
|
|
58
|
+
'get_name_known_for': '/name/{imdb_id}/knownfor',
|
|
59
|
+
'get_name_awards': '/name/{imdb_id}/awards',
|
|
60
|
+
'get_name_bio': '/name/{imdb_id}/bio',
|
|
61
|
+
'get_name_trivia': '/name/{imdb_id}/trivia',
|
|
62
|
+
'get_name_quotes': '/name/{imdb_id}/quotes',
|
|
63
|
+
'get_name_news': '/name/{imdb_id}/news',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Imdb(Auth):
|
|
68
|
+
CACHE_MAX_ENTRIES = 128
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
locale=None,
|
|
73
|
+
exclude_episodes=False,
|
|
74
|
+
session=None,
|
|
75
|
+
cache_ttl=300,
|
|
76
|
+
):
|
|
77
|
+
super().__init__()
|
|
78
|
+
self.locale = locale or 'en_US'
|
|
79
|
+
self.region = self.locale.split('_')[-1].upper()
|
|
80
|
+
self.exclude_episodes = exclude_episodes
|
|
81
|
+
self.session = session or requests.Session()
|
|
82
|
+
self._cachedir = tempfile.gettempdir()
|
|
83
|
+
self.cache_ttl = cache_ttl
|
|
84
|
+
self._resource_cache = {}
|
|
85
|
+
|
|
86
|
+
def __getattr__(self, name):
|
|
87
|
+
if name in _SIMPLE_GET_ENDPOINTS:
|
|
88
|
+
return self._simple_get_method(
|
|
89
|
+
method=name, path=_SIMPLE_GET_ENDPOINTS[name]
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
msg = '{obj!r} object has no attribute {name!r}'.format(
|
|
93
|
+
obj=self.__class__.__name__,
|
|
94
|
+
name=name,
|
|
95
|
+
)
|
|
96
|
+
raise AttributeError(msg)
|
|
97
|
+
|
|
98
|
+
def get_title(self, imdb_id):
|
|
99
|
+
logger.info('called get_title %s', imdb_id)
|
|
100
|
+
self.validate_imdb_id(imdb_id)
|
|
101
|
+
# redirection check using API response
|
|
102
|
+
self._redirection_title_check(imdb_id)
|
|
103
|
+
try:
|
|
104
|
+
resource = self._get_resource_cached(
|
|
105
|
+
'/title/{0}/auxiliary'.format(imdb_id)
|
|
106
|
+
)
|
|
107
|
+
except LookupError:
|
|
108
|
+
self._title_not_found()
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
self.exclude_episodes is True
|
|
112
|
+
and resource['base']['titleType'] == 'tvEpisode'
|
|
113
|
+
):
|
|
114
|
+
raise LookupError(
|
|
115
|
+
'Title not found. Title was an episode and '
|
|
116
|
+
'"exclude_episodes" is set to true'
|
|
117
|
+
)
|
|
118
|
+
return resource
|
|
119
|
+
|
|
120
|
+
def _fetch_title_auxiliary(self, imdb_id):
|
|
121
|
+
path = '/template/imdb-ios-writable/title-auxiliary-v31.jstl/render'
|
|
122
|
+
return self._get_cached(
|
|
123
|
+
url=urljoin(BASE_URI, path),
|
|
124
|
+
params={
|
|
125
|
+
'inlineBannerAdWeblabOn': 'false',
|
|
126
|
+
'minwidth': '320',
|
|
127
|
+
'osVersion': '11.3.0',
|
|
128
|
+
'region': self.region,
|
|
129
|
+
'tconst': imdb_id,
|
|
130
|
+
'today': date.today().strftime('%Y-%m-%d'),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def get_title_auxiliary(self, imdb_id):
|
|
135
|
+
logger.info('called get_title_auxiliary %s', imdb_id)
|
|
136
|
+
self.validate_imdb_id(imdb_id)
|
|
137
|
+
# redirection check using API response
|
|
138
|
+
self._redirection_title_check(imdb_id)
|
|
139
|
+
try:
|
|
140
|
+
resource = self._fetch_title_auxiliary(imdb_id)
|
|
141
|
+
except LookupError:
|
|
142
|
+
self._title_not_found()
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
self.exclude_episodes is True
|
|
146
|
+
and resource['titleType'].lower() == 'tvepisode'
|
|
147
|
+
):
|
|
148
|
+
raise LookupError(
|
|
149
|
+
'Title not found. Title was an episode and '
|
|
150
|
+
'"exclude_episodes" is set to true'
|
|
151
|
+
)
|
|
152
|
+
return resource
|
|
153
|
+
|
|
154
|
+
def _simple_get_method(self, method, path):
|
|
155
|
+
"""Return client method generated from ``_SIMPLE_GET_ENDPOINTS``."""
|
|
156
|
+
|
|
157
|
+
def get(imdb_id):
|
|
158
|
+
logger.info('called %s %s', method, imdb_id)
|
|
159
|
+
self.validate_imdb_id(imdb_id)
|
|
160
|
+
self._redirection_title_check(imdb_id)
|
|
161
|
+
return self._get_resource(path.format(imdb_id=imdb_id))
|
|
162
|
+
|
|
163
|
+
return get
|
|
164
|
+
|
|
165
|
+
def title_exists(self, imdb_id):
|
|
166
|
+
self.validate_imdb_id(imdb_id)
|
|
167
|
+
try:
|
|
168
|
+
self._get_resource('/title/{0}/auxiliary'.format(imdb_id))
|
|
169
|
+
return True
|
|
170
|
+
except LookupError:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def _suggest_search(self, query):
|
|
174
|
+
# translates national characters into similar sounding latin characters
|
|
175
|
+
cleaned_query = unidecode(query)
|
|
176
|
+
query_encoded = quote(cleaned_query)
|
|
177
|
+
first_alphanum_char = self._query_first_alpha_num(cleaned_query)
|
|
178
|
+
path = '/suggests/{0}/{1}.json'.format(
|
|
179
|
+
first_alphanum_char, query_encoded
|
|
180
|
+
)
|
|
181
|
+
url = urljoin(SEARCH_BASE_URI, path)
|
|
182
|
+
search_results = self._get(url=url, query=query_encoded)
|
|
183
|
+
return search_results
|
|
184
|
+
|
|
185
|
+
def search_for_name(self, name):
|
|
186
|
+
logger.info('called search_for_name %s', name)
|
|
187
|
+
name = re.sub(r'\W+', '+', name).strip('+')
|
|
188
|
+
search_results = self._suggest_search(name)
|
|
189
|
+
results = []
|
|
190
|
+
for result in search_results.get('d', ()):
|
|
191
|
+
if not result['id'].startswith('nm'):
|
|
192
|
+
# ignore non-person results
|
|
193
|
+
continue
|
|
194
|
+
result_item = {
|
|
195
|
+
'name': result['l'],
|
|
196
|
+
'imdb_id': result['id'],
|
|
197
|
+
}
|
|
198
|
+
results.append(result_item)
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
def search_for_title(self, title):
|
|
202
|
+
logger.info('called search_for_title %s', title)
|
|
203
|
+
title = re.sub(r'\W+', '+', title).strip('+')
|
|
204
|
+
search_results = self._suggest_search(title)
|
|
205
|
+
results = []
|
|
206
|
+
for result in search_results.get('d', ()):
|
|
207
|
+
if not result['id'].startswith('tt'):
|
|
208
|
+
# ignore non-title results
|
|
209
|
+
continue
|
|
210
|
+
result_item = {
|
|
211
|
+
'title': result['l'],
|
|
212
|
+
'year': str(result['y']) if result.get('y') else None,
|
|
213
|
+
'imdb_id': result['id'],
|
|
214
|
+
'type': result.get('q'),
|
|
215
|
+
}
|
|
216
|
+
results.append(result_item)
|
|
217
|
+
return results
|
|
218
|
+
|
|
219
|
+
def get_popular_titles(self):
|
|
220
|
+
return self._get_resource('/chart/titlemeter')
|
|
221
|
+
|
|
222
|
+
def get_popular_shows(self):
|
|
223
|
+
return self._get_resource('/chart/tvmeter')
|
|
224
|
+
|
|
225
|
+
def get_popular_movies(self):
|
|
226
|
+
return self._get_resource('/chart/moviemeter')
|
|
227
|
+
|
|
228
|
+
def get_title_episodes(self, imdb_id):
|
|
229
|
+
logger.info('called get_title_episodes %s', imdb_id)
|
|
230
|
+
self.validate_imdb_id(imdb_id)
|
|
231
|
+
if self.exclude_episodes:
|
|
232
|
+
raise ValueError('exclude_episodes is current set to true')
|
|
233
|
+
return self._get_resource('/title/{0}/episodes'.format(imdb_id))
|
|
234
|
+
|
|
235
|
+
def get_title_episodes_detailed(
|
|
236
|
+
self, imdb_id, season, limit=500, region=None, offset=0
|
|
237
|
+
):
|
|
238
|
+
"""
|
|
239
|
+
Request detailed information for a tv series, for a specific season.
|
|
240
|
+
|
|
241
|
+
:param imdb_id: The imdb id including the TT prefix.
|
|
242
|
+
:param limit: Limit the amound of episodes returned for a season.
|
|
243
|
+
:param region: Two capital letter region code in ISO 3166-1 alpha-2.
|
|
244
|
+
:param season: The season you want the detailed information for.
|
|
245
|
+
:param offset: Offset episode results by this value.
|
|
246
|
+
"""
|
|
247
|
+
logger.info('called get_title_episodes_detailed %s', imdb_id)
|
|
248
|
+
self.validate_imdb_id(imdb_id)
|
|
249
|
+
if season < 1:
|
|
250
|
+
raise ValueError('season must be greater than zero')
|
|
251
|
+
params = {
|
|
252
|
+
'end': limit,
|
|
253
|
+
'start': offset,
|
|
254
|
+
'season': season - 1, # api seasons are zero indexed
|
|
255
|
+
'tconst': imdb_id,
|
|
256
|
+
}
|
|
257
|
+
if region:
|
|
258
|
+
params.update({'region': region})
|
|
259
|
+
|
|
260
|
+
return self._get(
|
|
261
|
+
urljoin(
|
|
262
|
+
BASE_URI,
|
|
263
|
+
'/template/imdb-ios-writable/tv-episodes-v2.jstl/render',
|
|
264
|
+
),
|
|
265
|
+
params=params,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def get_title_top_crew(self, imdb_id):
|
|
269
|
+
"""
|
|
270
|
+
Request detailed information about title's top crew
|
|
271
|
+
(ie: directors, writters, etc.).
|
|
272
|
+
|
|
273
|
+
:param imdb_id: The imdb id including the TT prefix.
|
|
274
|
+
"""
|
|
275
|
+
logger.info('called get_title_top_crew %s', imdb_id)
|
|
276
|
+
self.validate_imdb_id(imdb_id)
|
|
277
|
+
params = {'tconst': imdb_id}
|
|
278
|
+
return self._get(
|
|
279
|
+
urljoin(
|
|
280
|
+
BASE_URI,
|
|
281
|
+
'/template/imdb-android-writable/7.3.top-crew.jstl/render',
|
|
282
|
+
),
|
|
283
|
+
params=params,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _parse_dirty_json(data, query=None):
|
|
288
|
+
if query is None:
|
|
289
|
+
match_json_within_dirty_json = r'imdb\$.+\({1}(.+)\){1}'
|
|
290
|
+
else:
|
|
291
|
+
# No need to unquote as the json is containing quoted query
|
|
292
|
+
match_json_within_dirty_json = r'imdb\${}\((.+)\)'.format(query)
|
|
293
|
+
data_clean = re.match(
|
|
294
|
+
match_json_within_dirty_json, data, re.IGNORECASE
|
|
295
|
+
).groups()[0]
|
|
296
|
+
return json.loads(data_clean)
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def validate_imdb_id(imdb_id):
|
|
300
|
+
match_id = r'[a-zA-Z]{2}[0-9]{7,8}'
|
|
301
|
+
try:
|
|
302
|
+
re.match(match_id, imdb_id, re.IGNORECASE).group()
|
|
303
|
+
except (AttributeError, TypeError):
|
|
304
|
+
raise ValueError('invalid imdb id')
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _is_redirection_result(response):
|
|
308
|
+
"""
|
|
309
|
+
Return True if response is that of a redirection else False
|
|
310
|
+
Redirection results have no information of use.
|
|
311
|
+
"""
|
|
312
|
+
imdb_id = response['data'].get('tconst')
|
|
313
|
+
if imdb_id and imdb_id != response['data'].get('news', {}).get(
|
|
314
|
+
'channel'
|
|
315
|
+
):
|
|
316
|
+
return True
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
def _get_resource(self, path):
|
|
320
|
+
url = urljoin(BASE_URI, path)
|
|
321
|
+
return self._get(url=url)['resource']
|
|
322
|
+
|
|
323
|
+
def _get_resource_cached(self, path):
|
|
324
|
+
url = urljoin(BASE_URI, path)
|
|
325
|
+
return self._get_cached(url=url)['resource']
|
|
326
|
+
|
|
327
|
+
def clear_cache(self):
|
|
328
|
+
"""Drop resources cached by this client instance."""
|
|
329
|
+
self._resource_cache.clear()
|
|
330
|
+
|
|
331
|
+
def _evict_cache(self, now):
|
|
332
|
+
for key, (expires_at, _) in list(self._resource_cache.items()):
|
|
333
|
+
if expires_at <= now:
|
|
334
|
+
del self._resource_cache[key]
|
|
335
|
+
while len(self._resource_cache) >= self.CACHE_MAX_ENTRIES:
|
|
336
|
+
oldest = next(iter(self._resource_cache))
|
|
337
|
+
del self._resource_cache[oldest]
|
|
338
|
+
|
|
339
|
+
def _get_cached(self, url, params=None):
|
|
340
|
+
"""``_get`` with a ``cache_ttl`` second memory cache."""
|
|
341
|
+
if not self.cache_ttl:
|
|
342
|
+
return self._get(url=url, params=params)
|
|
343
|
+
key = (url, tuple(sorted((params or {}).items())))
|
|
344
|
+
cached = self._resource_cache.get(key)
|
|
345
|
+
now = time.monotonic()
|
|
346
|
+
if cached is not None and cached[0] > now:
|
|
347
|
+
# Hand out a copy so callers cannot mutate the cached payload.
|
|
348
|
+
return copy.deepcopy(cached[1])
|
|
349
|
+
resource = self._get(url=url, params=params)
|
|
350
|
+
if resource is not None:
|
|
351
|
+
# Do not cache error responses, they clear up on the next call.
|
|
352
|
+
self._evict_cache(now)
|
|
353
|
+
self._resource_cache[key] = (
|
|
354
|
+
now + self.cache_ttl,
|
|
355
|
+
copy.deepcopy(resource),
|
|
356
|
+
)
|
|
357
|
+
return resource
|
|
358
|
+
|
|
359
|
+
def _get(self, url, query=None, params=None):
|
|
360
|
+
headers = {'Accept-Language': self.locale}
|
|
361
|
+
if params:
|
|
362
|
+
full_url = '{0}?{1}'.format(url, urlencode(params))
|
|
363
|
+
else:
|
|
364
|
+
full_url = url
|
|
365
|
+
headers.update(self.get_auth_headers(full_url))
|
|
366
|
+
resp = self.session.get(url, headers=headers, params=params)
|
|
367
|
+
if not resp.ok:
|
|
368
|
+
if resp.status_code == httplib.NOT_FOUND:
|
|
369
|
+
raise LookupError('Resource {0} not found'.format(url))
|
|
370
|
+
else:
|
|
371
|
+
msg = '{0} {1}'.format(resp.status_code, resp.text)
|
|
372
|
+
raise ImdbAPIError(msg)
|
|
373
|
+
resp_data = resp.content.decode('utf-8')
|
|
374
|
+
try:
|
|
375
|
+
resp_dict = json.loads(resp_data)
|
|
376
|
+
except ValueError:
|
|
377
|
+
resp_dict = self._parse_dirty_json(data=resp_data, query=query)
|
|
378
|
+
|
|
379
|
+
if resp_dict.get('error'):
|
|
380
|
+
return None
|
|
381
|
+
return resp_dict
|
|
382
|
+
|
|
383
|
+
def _redirection_title_check(self, imdb_id):
|
|
384
|
+
if self.is_redirection_title(imdb_id):
|
|
385
|
+
self._title_not_found(
|
|
386
|
+
msg='{0} is a redirection imdb id'.format(imdb_id)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def is_redirection_title(self, imdb_id):
|
|
390
|
+
self.validate_imdb_id(imdb_id)
|
|
391
|
+
try:
|
|
392
|
+
if imdb_id.startswith('nm'):
|
|
393
|
+
resource = self._get_resource_cached(
|
|
394
|
+
'/name/{0}/fulldetails'.format(imdb_id)
|
|
395
|
+
)
|
|
396
|
+
returned_id = resource['base'].get('id', '')
|
|
397
|
+
if returned_id:
|
|
398
|
+
match = re.search(r'nm\d{7,8}', returned_id)
|
|
399
|
+
if match:
|
|
400
|
+
actual_id = match.group()
|
|
401
|
+
# If the returned ID differs, it's a redirection
|
|
402
|
+
return actual_id != imdb_id
|
|
403
|
+
else:
|
|
404
|
+
resource = self._fetch_title_auxiliary(imdb_id)
|
|
405
|
+
returned_id = resource.get('id', '')
|
|
406
|
+
if returned_id:
|
|
407
|
+
match = re.search(r'tt\d{7,8}', returned_id)
|
|
408
|
+
if match:
|
|
409
|
+
actual_id = match.group()
|
|
410
|
+
# If the returned ID differs, it's a redirection
|
|
411
|
+
return actual_id != imdb_id
|
|
412
|
+
return False
|
|
413
|
+
except LookupError:
|
|
414
|
+
# If ID doesn't exist at all, it's not a redirection
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def _query_first_alpha_num(query):
|
|
419
|
+
for char in query.lower():
|
|
420
|
+
if char.isalnum():
|
|
421
|
+
return char
|
|
422
|
+
raise ValueError(
|
|
423
|
+
'invalid query, does not contain any alphanumeric characters'
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def _title_not_found(msg=''):
|
|
428
|
+
if msg:
|
|
429
|
+
msg = ' {0}'.format(msg)
|
|
430
|
+
raise LookupError('Title not found.{0}'.format(msg))
|
imdbpie/objects.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import attr
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@attr.s
|
|
5
|
+
class Image(object):
|
|
6
|
+
url = attr.ib()
|
|
7
|
+
width = attr.ib()
|
|
8
|
+
height = attr.ib()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@attr.s
|
|
12
|
+
class TitleVideo(object):
|
|
13
|
+
content_type = attr.ib(default=None)
|
|
14
|
+
description = attr.ib(default=None)
|
|
15
|
+
url = attr.ib(default=None)
|
|
16
|
+
duration_seconds = attr.ib(default=None)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@attr.s
|
|
20
|
+
class WatchOption(object):
|
|
21
|
+
provider = attr.ib()
|
|
22
|
+
category = attr.ib()
|
|
23
|
+
url = attr.ib(default=None)
|
|
24
|
+
provider_id = attr.ib(default=None)
|
|
25
|
+
notes = attr.ib(default=None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@attr.s
|
|
29
|
+
class TitleBusiness(object):
|
|
30
|
+
budget = attr.ib(default=None)
|
|
31
|
+
worldwide_gross = attr.ib(default=None)
|
|
32
|
+
gross = attr.ib(default=())
|
|
33
|
+
opening_weekend_gross = attr.ib(default=())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@attr.s
|
|
37
|
+
class TitleCertificate(object):
|
|
38
|
+
country = attr.ib()
|
|
39
|
+
certificate = attr.ib()
|
|
40
|
+
attributes = attr.ib(default=())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@attr.s
|
|
44
|
+
class FilmingLocation(object):
|
|
45
|
+
location = attr.ib()
|
|
46
|
+
attributes = attr.ib(default=())
|
|
47
|
+
notes = attr.ib(default=())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@attr.s
|
|
51
|
+
class Award(object):
|
|
52
|
+
name = attr.ib()
|
|
53
|
+
category = attr.ib()
|
|
54
|
+
year = attr.ib(default=None)
|
|
55
|
+
won = attr.ib(default=False)
|
|
56
|
+
event_name = attr.ib(default=None)
|
|
57
|
+
title_ids = attr.ib(default=())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TitleEpisodes(object):
|
|
61
|
+
def __init__(self, facade, imdb_id):
|
|
62
|
+
self._facade = facade
|
|
63
|
+
episodes = self._facade._client.get_title_episodes(imdb_id=imdb_id)
|
|
64
|
+
self._episode_imdb_ids = []
|
|
65
|
+
for season in episodes['seasons']:
|
|
66
|
+
for episode in season['episodes']:
|
|
67
|
+
imdb_id = self._facade._parse_id(episode['id'])
|
|
68
|
+
self._episode_imdb_ids.append(imdb_id)
|
|
69
|
+
self._count = len(self._episode_imdb_ids)
|
|
70
|
+
|
|
71
|
+
def __len__(self):
|
|
72
|
+
return self._count
|
|
73
|
+
|
|
74
|
+
def __bool__(self):
|
|
75
|
+
return self._count > 0
|
|
76
|
+
|
|
77
|
+
def __getitem__(self, index):
|
|
78
|
+
imdb_id = self._episode_imdb_ids[index]
|
|
79
|
+
return self._facade.get_title(imdb_id=imdb_id)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@attr.s
|
|
83
|
+
class Title(object):
|
|
84
|
+
imdb_id = attr.ib()
|
|
85
|
+
title = attr.ib()
|
|
86
|
+
type = attr.ib()
|
|
87
|
+
certification = attr.ib()
|
|
88
|
+
year = attr.ib()
|
|
89
|
+
genres = attr.ib()
|
|
90
|
+
writers = attr.ib()
|
|
91
|
+
creators = attr.ib()
|
|
92
|
+
credits = attr.ib()
|
|
93
|
+
directors = attr.ib()
|
|
94
|
+
stars = attr.ib()
|
|
95
|
+
image = attr.ib()
|
|
96
|
+
episodes = attr.ib()
|
|
97
|
+
rating_count = attr.ib(default=0)
|
|
98
|
+
releases = attr.ib(default=())
|
|
99
|
+
season = attr.ib(default=None)
|
|
100
|
+
episode = attr.ib(default=None)
|
|
101
|
+
rating = attr.ib(default=None)
|
|
102
|
+
plot_outline = attr.ib(default=None)
|
|
103
|
+
release_date = attr.ib(default=None)
|
|
104
|
+
runtime = attr.ib(default=None)
|
|
105
|
+
metacritic = attr.ib(default=None)
|
|
106
|
+
awards_summary = attr.ib(default=None)
|
|
107
|
+
spoken_languages = attr.ib(default=())
|
|
108
|
+
countries = attr.ib(default=())
|
|
109
|
+
alternate_titles = attr.ib(default=())
|
|
110
|
+
alternate_titles_count = attr.ib(default=0)
|
|
111
|
+
top_rank = attr.ib(default=None)
|
|
112
|
+
trailer = attr.ib(default=None)
|
|
113
|
+
production_status = attr.ib(default=None)
|
|
114
|
+
certificate_reason = attr.ib(default=None)
|
|
115
|
+
|
|
116
|
+
def __repr__(self):
|
|
117
|
+
return 'Title(imdb_id={0}, title={1})'.format(self.imdb_id, self.title)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@attr.s
|
|
121
|
+
class TitleSearchResult(object):
|
|
122
|
+
imdb_id = attr.ib()
|
|
123
|
+
title = attr.ib()
|
|
124
|
+
type = attr.ib()
|
|
125
|
+
year = attr.ib()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@attr.s
|
|
129
|
+
class NameSearchResult(object):
|
|
130
|
+
imdb_id = attr.ib()
|
|
131
|
+
name = attr.ib()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@attr.s
|
|
135
|
+
class TitleRelease(object):
|
|
136
|
+
date = attr.ib()
|
|
137
|
+
region = attr.ib()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@attr.s
|
|
141
|
+
class TitleName(object):
|
|
142
|
+
name = attr.ib()
|
|
143
|
+
category = attr.ib()
|
|
144
|
+
imdb_id = attr.ib()
|
|
145
|
+
job = attr.ib(default=None)
|
|
146
|
+
characters = attr.ib(default=())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@attr.s
|
|
150
|
+
class Name(object):
|
|
151
|
+
name = attr.ib()
|
|
152
|
+
imdb_id = attr.ib()
|
|
153
|
+
image = attr.ib()
|
|
154
|
+
birth_place = attr.ib()
|
|
155
|
+
gender = attr.ib()
|
|
156
|
+
bios = attr.ib()
|
|
157
|
+
date_of_birth = attr.ib()
|
|
158
|
+
filmography = attr.ib()
|
|
159
|
+
jobs = attr.ib(default=())
|
|
160
|
+
known_for = attr.ib(default=())
|
|
161
|
+
real_name = attr.ib(default=None)
|
|
162
|
+
height_centimeters = attr.ib(default=None)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imdbpie
|
|
3
|
+
Version: 5.8.0
|
|
4
|
+
Summary: Python IMDB client using the IMDB json web service made available for their iOS app.
|
|
5
|
+
Author-email: Richard O'Dwyer <richard@richard.do>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://code.richard.do/richardARPANET/imdb-pie/
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/x-rst
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: attrs>=26.1.0
|
|
17
|
+
Requires-Dist: boto>=2.49.0
|
|
18
|
+
Requires-Dist: botocore>=1.42.83
|
|
19
|
+
Requires-Dist: diskcache-community>=5.8.3
|
|
20
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
21
|
+
Requires-Dist: requests>=2.33.1
|
|
22
|
+
Requires-Dist: setuptools>=82.0.1
|
|
23
|
+
Requires-Dist: text-unidecode>=1.3
|
|
24
|
+
Requires-Dist: wheel>=0.46.3
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
ImdbPie
|
|
28
|
+
=======
|
|
29
|
+
|
|
30
|
+
|PyPI| |Python Versions|
|
|
31
|
+
|
|
32
|
+
Python IMDB client using the IMDB JSON web service made available for their iOS application.
|
|
33
|
+
|
|
34
|
+
API Terminology
|
|
35
|
+
---------------
|
|
36
|
+
|
|
37
|
+
- ``Title`` this can be a movie, tv show, video, documentary etc.
|
|
38
|
+
- ``Name`` this can be a credit, cast member, any person generally.
|
|
39
|
+
|
|
40
|
+
Installation
|
|
41
|
+
------------
|
|
42
|
+
|
|
43
|
+
To install imdbpie, simply:
|
|
44
|
+
|
|
45
|
+
.. code:: bash
|
|
46
|
+
|
|
47
|
+
uv add imdbpie
|
|
48
|
+
|
|
49
|
+
How to use
|
|
50
|
+
------------
|
|
51
|
+
|
|
52
|
+
Choose an option:
|
|
53
|
+
|
|
54
|
+
1. `ImdbPie Facade usage examples <FACADE.rst>`_ (the easy way, returns objects).
|
|
55
|
+
|
|
56
|
+
2. `ImdbPie Client usage examples <CLIENT.rst>`_ (more low level client API, returns raw dicts).
|
|
57
|
+
|
|
58
|
+
Requirements
|
|
59
|
+
------------
|
|
60
|
+
|
|
61
|
+
::
|
|
62
|
+
|
|
63
|
+
1. Python 3.10 or newer
|
|
64
|
+
2. Dependencies are declared in pyproject.toml and pinned in uv.lock
|
|
65
|
+
|
|
66
|
+
Development
|
|
67
|
+
-----------
|
|
68
|
+
|
|
69
|
+
This project uses `uv <https://docs.astral.sh/uv/>`_ for environments,
|
|
70
|
+
dependencies and locking.
|
|
71
|
+
|
|
72
|
+
.. code:: bash
|
|
73
|
+
|
|
74
|
+
uv sync
|
|
75
|
+
|
|
76
|
+
Running the tests
|
|
77
|
+
-----------------
|
|
78
|
+
|
|
79
|
+
.. code:: bash
|
|
80
|
+
|
|
81
|
+
uv run pytest src/tests
|
|
82
|
+
|
|
83
|
+
Some tests call the live IMDb API and are marked ``live``. Skip them when
|
|
84
|
+
working offline:
|
|
85
|
+
|
|
86
|
+
.. code:: bash
|
|
87
|
+
|
|
88
|
+
uv run pytest src/tests -m "not live"
|
|
89
|
+
|
|
90
|
+
.. |PyPI| image:: https://img.shields.io/pypi/v/imdbpie.svg
|
|
91
|
+
:target: https://pypi.python.org/pypi/imdb-pie
|
|
92
|
+
.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/imdbpie.svg
|
|
93
|
+
:target: https://pypi.python.org/pypi/imdb-pie
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
imdbpie/__init__.py,sha256=q4MdNaE0WwCfodXbYENvHnpHNAwbXTruLHF6lq_E97c,142
|
|
2
|
+
imdbpie/auth.py,sha256=IwPeYfacjZC2HqhRe41VN7XtWBjl_CwTIFAph6a9OdY,3190
|
|
3
|
+
imdbpie/constants.py,sha256=26DT7y1jKwI_eVuVyjL6js9IN2OxYGxkAsF4Gzr8Gb0,292
|
|
4
|
+
imdbpie/exceptions.py,sha256=gmYjoIWWd7kQGlIGHKO00261ElQDEo-pnZ1tXXvX4kM,64
|
|
5
|
+
imdbpie/facade.py,sha256=74Qkdds2Qp-Oir1Eye5DjmdUUqDWOnWeCvw0vYbQuDk,14861
|
|
6
|
+
imdbpie/imdbpie.py,sha256=pFBt251C554d0sD96jM5YoqGThXDLnIl-rPZul22ylQ,15819
|
|
7
|
+
imdbpie/objects.py,sha256=P_0rbO8EFSYh1LCozfL30-yJCI2AoiB5H1X9ptRDAeg,3908
|
|
8
|
+
imdbpie-5.8.0.dist-info/licenses/LICENSE,sha256=i42eLltK2Nu2C2HUnml4JMKcAz582pLMyxeOShb70Sg,583
|
|
9
|
+
imdbpie-5.8.0.dist-info/METADATA,sha256=KA4U4kAGHme6iMV1xY3DJODSjPSJtVVIxB6iahIEIqU,2317
|
|
10
|
+
imdbpie-5.8.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
11
|
+
imdbpie-5.8.0.dist-info/top_level.txt,sha256=FPdYez0bUsp6xnEuNFanitCDlm6gO11optSdUfQk4wE,8
|
|
12
|
+
imdbpie-5.8.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2017 Richard O'Dwyer
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
imdbpie
|