-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathjustwatch.py
More file actions
620 lines (466 loc) · 24.2 KB
/
justwatch.py
File metadata and controls
620 lines (466 loc) · 24.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
"""
Main functions used for obtaining data from JustWatch GraphQL API.
Each function sends **one** GraphQL query to JustWatch API and returns API response
parsed into a [`NamedTuple`][typing.NamedTuple] from [`tuples`]
[simplejustwatchapi.tuples] module. Everything is handled on the API side through
prepared GraphQL query.
Most functions have a number of common arguments (in addition to function-specific
ones, like `title` to search for):
| Name | Description |
|------|-------------|
| `country` | 2-letter country code for which offers are selected, (e.g., `US`, \
`GB`, `DE`). |
| `language` | Code for language in responses. It consists of 2 lowercase letters \
with optional uppercase alphanumeric suffix (e.g., `en`, `en-US`, \
`de`, `de-CH1901`). |
| `best_only` | Whether to return only "best" offers for each provider instead of, \
e.g., separate offer for SD, HD, and 4K. |
Functions returning data for multiple titles
([`search`][simplejustwatchapi.justwatch.search],
[`popular`][simplejustwatchapi.justwatch.popular])
also allow for specifying number of elements, basic pagination, and additional
filtering:
| Name | Description |
|------|-------------|
| `count` | How many entries should be returned. |
| `offset` | Basic "pagination". Offset for the first returned result, i.e. how \
many first entries should be skipped. Everything is handled on API \
side, this library isn't doing any filtering. |
| `providers` | Providers (like Netflix, Amazon Prime Video) for which offers should \
returned. Requires 3-letter "short name". Check \
[`providers`][simplejustwatchapi.justwatch.providers] for an example \
of how you can get that value. |
| `min_release_year` | Minimum release year of returned titles. |
| `max_release_year` | Maximum release year of returned titles. |
| `object_types` | Types of objects to filter for. It seems that only `SHOW` and \
`MOVIE` are useful, but it's not strictly enforced. |
Each function can raise two exceptions:
| Exception | Cause |
|-----------|-------|
| [`JustWatchHttpError`][simplejustwatchapi.exceptions.JustWatchHttpError] | \
HTTP error occurred, e.g., JustWatch API responded with non-`2xx` status code. |
| [`JustWatchApiError`][simplejustwatchapi.exceptions.JustWatchApiError] | \
JSON response from JustWatch API contains errors, e.g., due to invalid language or \
country code. |
"""
from httpx import HTTPError, HTTPStatusError, post
from simplejustwatchapi.exceptions import JustWatchHttpError
from simplejustwatchapi.query import (
parse_details_response,
parse_episodes_response,
parse_offers_for_countries_response,
parse_popular_response,
parse_providers_response,
parse_search_response,
parse_seasons_response,
prepare_details_request,
prepare_episodes_request,
prepare_offers_for_countries_request,
prepare_popular_request,
prepare_providers_request,
prepare_search_request,
prepare_seasons_request,
)
from simplejustwatchapi.tuples import Episode, MediaEntry, Offer, OfferPackage
_GRAPHQL_API_URL = "https://apis.justwatch.com/graphql"
def search(
title: str = "",
country: str = "US",
language: str = "en",
count: int = 4,
best_only: bool = True,
offset: int = 0,
providers: list[str] | str | None = None,
min_release_year: int | None = None,
max_release_year: int | None = None,
object_types: list[str] | str | None = None,
) -> list[MediaEntry]:
"""
Search JustWatch for the given title.
If no `title` is provided (or an empty string, as per default value) you'll get a
selection of "popular" titles. Without `title` the output is very similar to
[`popular`][simplejustwatchapi.justwatch.popular] function.
Title isn't stripped, so passing string with only spaces will look for those spaces.
JustWatch API won't allow for getting more than 1999 responses, either through
`count`, or when `count + offset` is equal or greater than 2000 - it will return an
empty list instead (**always** an empty list, it won't include entries up to
1999th).
Args:
title (str): Title to search.
Not stripped, passed to the API as-is.
country (str): 2-letter country code for which offers are selected.
It seems to be **ISO 3166-1 alpha-2** standard (e.g., `EN`, `FR`, `DE`),
however the API doesn't specify it directly. It should be uppercase, however
it's normalized to uppercase automatically.
language (str): Code for language in responses (e.g., description, title).
It consists of 2 lowercase letters with optional uppercase alphanumeric
suffix after `-` symbol (e.g., `en`, `en-US`, `de`, `de-CH1901`). Similar to
**IETF BCP 47** standard, however the suffix can contain only uppercase
letters and numbers.
Its value isn't normalized and **must** be provided in expected format,
including letter case.
count (int): Return up to this many results.
Too high values can cause API errors due to too high operation complexity,
if you need more results, use with `offset` argument to get them in batches.
best_only (bool): Return only best offers if `True`, return all offers if
`False`.
If service offers the same title in 4K, HD, and SD, then `best_only = True`
returns only 4K, `best_only = False` returns all three.
offset (int): Offset for the first returned result, i.e. how many first entries
should be skipped.
This is done on API side, not the library side; the returned list is still
directly parsed from API response.
I'm not sure if it guarantees stability of results - if repeated calls to
this function with increasing offset will guarantee that you won't get
repeats.
providers (list[str] | str | None): Selection of 3-letter service identifiers
(e.g, `nfx` for "Netflix") to filter for.
For single provider you can either pass a single string, or a list of one
string. For `None` (the default value) no filtering is done.
Invalid codes will be ignored, however if all are invalid, then no filtering
is done.
You can look up values through [`providers`]
[simplejustwatchapi.justwatch.providers] function.
min_release_year (int | None): Minimum release year of returned titles.
If `None` (the default value), no filtering is done.
max_release_year (int | None): Maximum release year of returned titles.
If `None` (the default value), no filtering is done.
object_types (list[str] | str | None): Types of objects to filter for, like
`SHOW` or `MOVIE`.
It seems that only `SHOW` and `MOVIE` are useful, but it's not strictly
enforced. Types like `SHOW_EPISODE`, or `SHOW_SEASON` can be used, but they
seem to return TV shows, same as `SHOW`.
While the type value is not enforced, it **must** be a valid type, otherwise
API will respond with HTTP status code 422.
For single type it can be a single string, or a list with one string.
If `None` (the default value), no filtering is done.
Returns:
(list[MediaEntry]): List of tuples with details of search results.
Raises:
exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
due to invalid language or country code.
exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
responded with non-`2xx` status code.
"""
request = prepare_search_request(
title,
country,
language,
count,
best_only,
offset,
providers,
min_release_year,
max_release_year,
object_types,
)
response = _post_to_jw_graphql_api(request)
return parse_search_response(response)
def popular(
country: str = "US",
language: str = "en",
count: int = 4,
best_only: bool = True,
offset: int = 0,
providers: list[str] | str | None = None,
min_release_year: int | None = None,
max_release_year: int | None = None,
object_types: list[str] | str | None = None,
) -> list[MediaEntry]:
"""
Look up all currently popular titles on JustWatch.
This function returns similar values as [`search`]
[simplejustwatchapi.justwatch.search] with no `title` provided.
JustWatch API won't allow for getting more than 1999 responses, either through
`count`, or when `count + offset` is equal or greater than 2000 - it will return an
empty list instead (**always** an empty list, it won't include entries up to 1999).
Args:
country (str): 2-letter country code for which offers are selected.
It seems to be **ISO 3166-1 alpha-2** standard (e.g., `EN`, `FR`, `DE`),
however the API doesn't specify it directly. It should be uppercase, however
it's normalized to uppercase automatically.
language (str): Code for language in responses (e.g., description, title).
It consists of 2 lowercase letters with optional uppercase alphanumeric
suffix after `-` symbol (e.g., `en`, `en-US`, `de`, `de-CH1901`). Similar to
**IETF BCP 47** standard, however the suffix can contain only uppercase
letters and numbers.
Its value isn't normalized and **must** be provided in expected format,
including letter case.
count (int): Return up to this many results.
Too high values can cause API errors due to too high operation complexity,
if you need more results, use with `offset` argument to get them in batches.
best_only (bool): Return only best offers if `True`, return all offers if
`False`.
If service offers the same title in 4K, HD, and SD, then `best_only = True`
returns only 4K, `best_only = False` returns all three.
offset (int): Offset for the first returned result, i.e. how many first entries
should be skipped.
This is done on API side, not the library side; the returned list is still
directly parsed from API response.
I'm not sure if it guarantees stability of results - if repeated calls to
this function with increasing offset will guarantee that you won't get
repeats.
providers (list[str] | str | None): Selection of 3-letter service identifiers
(e.g, `nfx` for "Netflix") to filter for.
For single provider you can either pass a single string, or a list of one
string. For `None` (the default value) no filtering is done.
Invalid codes will be ignored, however if all are invalid, then no filtering
is done.
You can look up values through [`providers`]
[simplejustwatchapi.justwatch.providers] function.
min_release_year (int | None): Minimum release year of returned titles.
If `None` (the default value), no filtering is done.
max_release_year (int | None): Maximum release year of returned titles.
If `None` (the default value), no filtering is done.
object_types (list[str] | str | None): Types of objects to filter for, like
`SHOW` or `MOVIE`.
It seems that only `SHOW` and `MOVIE` are useful, but it's not strictly
enforced. Types like `SHOW_EPISODE`, or `SHOW_SEASON` can be used, but they
seem to return TV shows, same as `SHOW`.
While the type value is not enforced, it **must** be a valid type, otherwise
API will respond with HTTP status code 422.
For single type it can be a single string, or a list with one string.
If `None` (the default value), no filtering is done.
Returns:
(list[MediaEntry]): List of tuples with details of popular titles.
Raises:
exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
due to invalid language or country code.
exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
responded with non-`2xx` status code.
"""
request = prepare_popular_request(
country,
language,
count,
best_only,
offset,
providers,
min_release_year,
max_release_year,
object_types,
)
response = _post_to_jw_graphql_api(request)
return parse_popular_response(response)
def details(
node_id: str,
country: str = "US",
language: str = "en",
best_only: bool = True,
) -> MediaEntry:
"""
Get details of entry for a given ID.
`country` is a 2-letter country code for which offers are selected. It should be
uppercase, however it will be normalized to uppercase automatically. It **must** be
2-letters long. It looks like ISO 3166-1 alpha-2 standard, however API doesn't
specify exact standard. If unexpected code is used, then [`JustWatchApiError`]
[simplejustwatchapi.exceptions.JustWatchApiError] exception is raised, as the API
will respond with internal error.
`language` is a language code for language in response (e.g., description, title).
In most basic form it's 2 lowercase letters (e.g., `en`, `de`).
It can also contain alphanumeric (in uppercase) suffix after `-` symbol, most
likely used for regional variants (e.g., `en-US`, `de-CH`).
It looks like a subset of IETF BCP 47, however the suffix can contain only uppercase
letters and numbers. It's value isn't normalized and **must** be provided in
expected format, including letter case.
`best_only` allows filtering out redundant offers, e.g. when if provide offers
service in 4K, HD and SD, using `best_only = True` returns only 4K option,
`best_only = False` returns all three.
Args:
node_id (str): ID of an entry to look up.
country (str): 2-letter country code for which offers are selected.
It seems to be **ISO 3166-1 alpha-2** standard (e.g., `EN`, `FR`, `DE`),
however the API doesn't specify it directly. It should be uppercase, however
it's normalized to uppercase automatically.
language (str): Code for language in responses (e.g., description, title).
It consists of 2 lowercase letters with optional uppercase alphanumeric
suffix after `-` symbol (e.g., `en`, `en-US`, `de`, `de-CH1901`). Similar to
**IETF BCP 47** standard, however the suffix can contain only uppercase
letters and numbers.
Its value isn't normalized and **must** be provided in expected format,
including letter case.
best_only (bool): Return only best offers if `True`, return all offers if
`False`.
If service offers the same title in 4K, HD, and SD, then `best_only = True`
returns only 4K, `best_only = False` returns all three.
Returns:
(MediaEntry): Tuple with data about requested entry.
Raises:
exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
due to invalid language or country code.
exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
responded with non-`2xx` status code.
"""
request = prepare_details_request(node_id, country, language, best_only)
response = _post_to_jw_graphql_api(request)
return parse_details_response(response)
def seasons(
show_id: str, country: str = "US", language: str = "en", best_only: bool = True
) -> list[MediaEntry]:
"""
Get details of all seasons available for a given show ID.
You can use [`episodes`][simplejustwatchapi.justwatch.episodes] function to get
details for each episode of a single season.
Args:
show_id (str): ID of a show to look up seasons for.
country (str): 2-letter country code for which offers are selected.
It seems to be **ISO 3166-1 alpha-2** standard (e.g., `EN`, `FR`, `DE`),
however the API doesn't specify it directly. It should be uppercase, however
it's normalized to uppercase automatically.
language (str): Code for language in responses (e.g., description, title).
It consists of 2 lowercase letters with optional uppercase alphanumeric
suffix after `-` symbol (e.g., `en`, `en-US`, `de`, `de-CH1901`). Similar to
**IETF BCP 47** standard, however the suffix can contain only uppercase
letters and numbers.
Its value isn't normalized and **must** be provided in expected format,
including letter case.
best_only (bool): Return only best offers if `True`, return all offers if
`False`.
If service offers the same title in 4K, HD, and SD, then `best_only = True`
returns only 4K, `best_only = False` returns all three.
Returns:
(list[MediaEntry]): List of tuples with seasons data about requested show.
Raises:
exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
due to invalid language or country code.
exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
responded with non-`2xx` status code.
"""
request = prepare_seasons_request(show_id, country, language, best_only)
response = _post_to_jw_graphql_api(request)
return parse_seasons_response(response)
def episodes(
season_id: str, country: str = "US", language: str = "en", best_only: bool = True
) -> list[Episode]:
"""
Get details of all episodes available for a given season ID.
[`Episode`][simplejustwatchapi.tuples.Episode] tuple is a subset of[`MediaEntry`]
[simplejustwatchapi.tuples.MediaEntry], but with only episode-specific fields,
e.g., `episode_number`.
Args:
season_id (str): ID of season to look up episodes for.
country (str): 2-letter country code for which offers are selected.
It seems to be **ISO 3166-1 alpha-2** standard (e.g., `EN`, `FR`, `DE`),
however the API doesn't specify it directly. It should be uppercase, however
it's normalized to uppercase automatically.
language (str): Code for language in responses (e.g., description, title).
It consists of 2 lowercase letters with optional uppercase alphanumeric
suffix after `-` symbol (e.g., `en`, `en-US`, `de`, `de-CH1901`). Similar to
**IETF BCP 47** standard, however the suffix can contain only uppercase
letters and numbers.
Its value isn't normalized and **must** be provided in expected format,
including letter case.
best_only (bool): Return only best offers if `True`, return all offers if
`False`.
If service offers the same title in 4K, HD, and SD, then `best_only = True`
returns only 4K, `best_only = False` returns all three.
Returns:
(list[Episode]): List of tuples with episode data about requested season.
Raises:
exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
due to invalid language or country code.
exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
responded with non-`2xx` status code.
"""
request = prepare_episodes_request(season_id, country, language, best_only)
response = _post_to_jw_graphql_api(request)
return parse_episodes_response(response)
def offers_for_countries(
node_id: str,
countries: set[str],
language: str = "en",
best_only: bool = True,
) -> dict[str, list[Offer]]:
"""
Get offers for entry of given node ID for all given countries.
Returned `dict` has keys matching `countries` argument and values are list of found
offers. If no countries are passed (an empty set given as argument) empty dict is
returned.
Country codes passed as argument are case-insensitive, however keys in returned dict
will match them exactly. For example, for countries specified as:
```python
{"uK", "Us", "AU", "ca"}
```
Returned dict will have the following structure:
```python
{
"uK": [ ... offers ... ],
"Us": [ ... offers ... ],
"AU": [ ... offers ... ],
"ca": [ ... offers ... ],
}
```
Args:
node_id (str): ID of entry to look up offers for.
countries (set[str]): 2-letter country codes for which offers are selected.
They seem to match **ISO 3166-1 alpha-2** standard (e.g., `EN`, `FR`, `DE`),
however the API doesn't specify it directly. They should be uppercase,
however they're normalized to uppercase automatically.
language (str): Code for language in responses (e.g., description, title).
It consists of 2 lowercase letters with optional uppercase alphanumeric
suffix after `-` symbol (e.g., `en`, `en-US`, `de`, `de-CH1901`). Similar to
**IETF BCP 47** standard, however the suffix can contain only uppercase
letters and numbers.
Its value isn't normalized and **must** be provided in expected format,
including letter case.
best_only (bool): Return only best offers if `True`, return all offers if
`False`.
If service offers the same title in 4K, HD, and SD, then `best_only = True`
returns only 4K, `best_only = False` returns all three.
Returns:
(dict[str, list[Offer]]): Keys match values in `countries` and values are all
found offers for their respective countries.
Raises:
exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
due to invalid language or country code.
exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
responded with non-`2xx` status code.
"""
if not countries:
return {}
request = prepare_offers_for_countries_request(
node_id, countries, language, best_only
)
response = _post_to_jw_graphql_api(request)
return parse_offers_for_countries_response(response, countries)
def providers(country: str = "US") -> list[OfferPackage]:
"""
Look up all providers for the given country.
Args:
country (str): 2-letter country code for which offers are selected.
It seems to be **ISO 3166-1 alpha-2** standard (e.g., `EN`, `FR`, `DE`),
however the API doesn't specify it directly. It should be uppercase, however
it's normalized to uppercase automatically.
Returns:
(list[OfferPackage]): List of all found providers.
[`OfferPackage`][simplejustwatchapi.tuples.OfferPackage] tuple matches
values in [`Offer`][simplejustwatchapi.tuples.Offer] (and thus in
[`MediaEntry`][simplejustwatchapi.tuples.MediaEntry]), but the data
structure is the same, so the same tuple is reused.
Raises:
exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
due to invalid language or country code.
exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
responded with non-`2xx` status code.
"""
request = prepare_providers_request(country)
response = _post_to_jw_graphql_api(request)
return parse_providers_response(response)
def _post_to_jw_graphql_api(request_json: dict) -> dict:
"""
Send a GraphQL query, verify HTTP response, return API response JSON as `dict`.
Args:
request_json(dict): JSON with full request - GraphQL query and variables.
Returns:
(dict): JSON response from the API.
Raises:
exceptions.JustWatchHttpError: HTTP-related error occurred.
"""
try:
response = post(_GRAPHQL_API_URL, json=request_json)
response.raise_for_status()
return response.json()
except HTTPStatusError as e:
raise JustWatchHttpError(str(e), e.response.text) from e
except HTTPError as e:
raise JustWatchHttpError(str(e)) from e