simple-justwatch-python-api 0.14__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.
@@ -0,0 +1,9 @@
1
+ simplejustwatchapi/__init__.py,sha256=wMyheY9kEK_Paz-2VM402Mscm5AwUeTreaI64Mr_-eY,150
2
+ simplejustwatchapi/justwatch.py,sha256=A4sYgKxMxKMkeCQzO3d-Lw8tRZFBtrU020ZSj4Nb2xE,4436
3
+ simplejustwatchapi/main.py,sha256=wGgvhyq7EB7KZOLRLDBHC3PzshVpgP_HHBcyIpG-1ro,467
4
+ simplejustwatchapi/query.py,sha256=i-sigR331i0_TfkAEOP1r5EvhS7QpW_IzvvNDuKUH5s,16590
5
+ simple_justwatch_python_api-0.14.dist-info/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
6
+ simple_justwatch_python_api-0.14.dist-info/METADATA,sha256=wbuyQ5q8V9GDUI9LS4XeZr0KqfNZmf7fNjgeA58NFY0,50112
7
+ simple_justwatch_python_api-0.14.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
8
+ simple_justwatch_python_api-0.14.dist-info/top_level.txt,sha256=GjJ4IpR7I8dT5RfsV8uWv-QjrNr5aVlw7Qfkvj_EC1I,19
9
+ simple_justwatch_python_api-0.14.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ simplejustwatchapi
@@ -0,0 +1,2 @@
1
+ from simplejustwatchapi.justwatch import details, offers_for_countries, search
2
+ from simplejustwatchapi.query import MediaEntry, Offer, OfferPackage
@@ -0,0 +1,117 @@
1
+ """Main module orchestrating requests to JustWatch GraphQL API."""
2
+
3
+ from httpx import post
4
+
5
+ from simplejustwatchapi.query import (
6
+ MediaEntry,
7
+ Offer,
8
+ parse_details_response,
9
+ parse_offers_for_countries_response,
10
+ parse_search_response,
11
+ prepare_details_request,
12
+ prepare_offers_for_countries_request,
13
+ prepare_search_request,
14
+ )
15
+
16
+ _GRAPHQL_API_URL = "https://apis.justwatch.com/graphql"
17
+
18
+
19
+ def search(
20
+ title: str, country: str = "US", language: str = "en", count: int = 4, best_only: bool = True
21
+ ) -> list[MediaEntry]:
22
+ """Search JustWatch for given title.
23
+
24
+ Returns a list of entries up to ``count``.
25
+
26
+ ``best_only`` allows filtering out redundant offers, e.g. when if provide offers service
27
+ in 4K, HD and SD, using ``best_only = True`` returns only 4K option, ``best_only = False``
28
+ returns all three.
29
+
30
+ Args:
31
+ title: title to search
32
+ country: country to search for offers, ``US`` by default
33
+ language: language of responses, ``en`` by default
34
+ count: how many responses should be returned
35
+ best_only: return only best offers if ``True``, return all offers if ``False``
36
+
37
+ Returns:
38
+ List of ``MediaEntry`` NamedTuples parsed from JustWatch response
39
+ """
40
+ request = prepare_search_request(title, country, language, count, best_only)
41
+ response = post(_GRAPHQL_API_URL, json=request)
42
+ response.raise_for_status()
43
+ return parse_search_response(response.json())
44
+
45
+
46
+ def details(
47
+ node_id: str, country: str = "US", language: str = "en", best_only: bool = True
48
+ ) -> MediaEntry:
49
+ """Get details of entry for a given ID.
50
+
51
+ ``best_only`` allows filtering out redundant offers, e.g. when if provide offers service
52
+ in 4K, HD and SD, using ``best_only = True`` returns only 4K option, ``best_only = False``
53
+ returns all three.
54
+
55
+ Args:
56
+ node_id: ID of entry to look up
57
+ country: country to search for offers, ``US`` by default
58
+ language: language of responses, ``en`` by default
59
+ best_only: return only best offers if ``True``, return all offers if ``False``
60
+
61
+ Returns:
62
+ ``MediaEntry`` NamedTuple with data about requested entry.
63
+ """
64
+ request = prepare_details_request(node_id, country, language, best_only)
65
+ response = post(_GRAPHQL_API_URL, json=request)
66
+ response.raise_for_status()
67
+ return parse_details_response(response.json())
68
+
69
+
70
+ def offers_for_countries(
71
+ node_id: str, countries: set[str], language: str = "en", best_only: bool = True
72
+ ) -> dict[str, list[Offer]]:
73
+ """Get offers for entry of given node ID for all countries passed as argument.
74
+ Language argument only specifies format of price string, e.g. whether ".", or "," is used
75
+ in decimal fractions.
76
+
77
+ Returned dict has keys matching ``countries`` argument and values are list of found offers.
78
+ If no countries are passed (an empty set given as argument) empty dict is returned.
79
+
80
+ Country codes passed as argument are case-insensitive, however keys in returned dict will match
81
+ them exactly. E.g. for countries specified as:
82
+
83
+ .. code-block:: python
84
+
85
+ {"uK", "Us", "AU", "ca"}
86
+
87
+ returned dict will have the following structure:
88
+
89
+ .. code-block:: python
90
+
91
+ {
92
+ "uK": [ ... offers ... ],
93
+ "Us": [ ... offers ... ],
94
+ "AU": [ ... offers ... ],
95
+ "ca": [ ... offers ... ],
96
+ }
97
+
98
+ ``best_only`` allows filtering out redundant offers, e.g. when if provide offers service
99
+ in 4K, HD and SD, using ``best_only = True`` returns only 4K option, ``best_only = False``
100
+ returns all three.
101
+
102
+ Args:
103
+ node_id: ID of entry to look up offers for
104
+ countries: set of country codes to search for offers
105
+ language: language of responses, ``en`` by default
106
+ best_only: return only best offers if ``True``, return all offers if ``False``
107
+
108
+ Returns:
109
+ ``dict`` where keys match values in ``countries`` and keys are all found offers for their
110
+ respective countries
111
+ """
112
+ if not countries:
113
+ return {}
114
+ request = prepare_offers_for_countries_request(node_id, countries, language, best_only)
115
+ response = post(_GRAPHQL_API_URL, json=request)
116
+ response.raise_for_status()
117
+ return parse_offers_for_countries_response(response.json(), countries)
@@ -0,0 +1,21 @@
1
+ from justwatch import details, offers_for_countries, search
2
+ from query import MediaEntry, Offer, OfferPackage
3
+
4
+
5
+ def main():
6
+ response = search("The Matrix", "US", "en", 3, True)
7
+ print(response)
8
+ print()
9
+
10
+ response = details("tm10", "US", "en", False)
11
+ print(response)
12
+ print()
13
+
14
+ response = offers_for_countries("tm10", {"US", "GB", "FR"}, "en", True)
15
+ print(response)
16
+ print()
17
+
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main()
@@ -0,0 +1,539 @@
1
+ """Module responsible for creating GraphQL queries and parsing responses from JustWatch GraphQL API.
2
+ Parsed responses are returned as Python NamedTuples for easier access."""
3
+
4
+ from typing import NamedTuple
5
+
6
+ _DETAILS_URL = "https://justwatch.com"
7
+ _IMAGES_URL = "https://images.justwatch.com"
8
+
9
+ _GRAPHQL_DETAILS_QUERY = """
10
+ query GetTitleNode(
11
+ $nodeId: ID!,
12
+ $language: Language!,
13
+ $country: Country!,
14
+ $formatPoster: ImageFormat,
15
+ $formatOfferIcon: ImageFormat,
16
+ $profile: PosterProfile,
17
+ $backdropProfile: BackdropProfile,
18
+ $filter: OfferFilter!,
19
+ ) {
20
+ node(id: $nodeId) {
21
+ ...TitleDetails
22
+ __typename
23
+ }
24
+ __typename
25
+ }
26
+ """
27
+
28
+ _GRAPHQL_SEARCH_QUERY = """
29
+ query GetSearchTitles(
30
+ $searchTitlesFilter: TitleFilter!,
31
+ $country: Country!,
32
+ $language: Language!,
33
+ $first: Int!,
34
+ $formatPoster: ImageFormat,
35
+ $formatOfferIcon: ImageFormat,
36
+ $profile: PosterProfile,
37
+ $backdropProfile: BackdropProfile,
38
+ $filter: OfferFilter!,
39
+ ) {
40
+ popularTitles(
41
+ country: $country
42
+ filter: $searchTitlesFilter
43
+ first: $first
44
+ sortBy: POPULAR
45
+ sortRandomSeed: 0
46
+ ) {
47
+ edges {
48
+ node {
49
+ ...TitleDetails
50
+ __typename
51
+ }
52
+ __typename
53
+ }
54
+ __typename
55
+ }
56
+ }
57
+ """
58
+
59
+ _GRAPHQL_OFFERS_BY_COUNTRY_QUERY = """
60
+ query GetTitleOffers(
61
+ $nodeId: ID!,
62
+ $language: Language!,
63
+ $formatOfferIcon: ImageFormat,
64
+ $filter: OfferFilter!,
65
+ ) {{
66
+ node(id: $nodeId) {{
67
+ ... on MovieOrShow {{
68
+ {country_entries}
69
+ __typename
70
+ }}
71
+ __typename
72
+ }}
73
+ __typename
74
+ }}
75
+ """
76
+
77
+ _GRAPHQL_DETAILS_FRAGMENT = """
78
+ fragment TitleDetails on MovieOrShow {
79
+ id
80
+ objectId
81
+ objectType
82
+ content(country: $country, language: $language) {
83
+ title
84
+ fullPath
85
+ originalReleaseYear
86
+ originalReleaseDate
87
+ runtime
88
+ shortDescription
89
+ genres {
90
+ shortName
91
+ __typename
92
+ }
93
+ externalIds {
94
+ imdbId
95
+ __typename
96
+ }
97
+ posterUrl(profile: $profile, format: $formatPoster)
98
+ backdrops(profile: $backdropProfile, format: $formatPoster) {
99
+ backdropUrl
100
+ __typename
101
+ }
102
+ __typename
103
+ }
104
+ offers(country: $country, platform: WEB, filter: $filter) {
105
+ ...TitleOffer
106
+ }
107
+ __typename
108
+ }
109
+ """
110
+
111
+ _GRAPHQL_OFFER_FRAGMENT = """
112
+ fragment TitleOffer on Offer {
113
+ id
114
+ monetizationType
115
+ presentationType
116
+ retailPrice(language: $language)
117
+ retailPriceValue
118
+ currency
119
+ lastChangeRetailPriceValue
120
+ type
121
+ package {
122
+ id
123
+ packageId
124
+ clearName
125
+ technicalName
126
+ icon(profile: S100, format: $formatOfferIcon)
127
+ __typename
128
+ }
129
+ standardWebURL
130
+ elementCount
131
+ availableTo
132
+ deeplinkRoku: deeplinkURL(platform: ROKU_OS)
133
+ subtitleLanguages
134
+ videoTechnology
135
+ audioTechnology
136
+ audioLanguages
137
+ __typename
138
+ }
139
+ """
140
+
141
+ _GRAPHQL_COUNTRY_OFFERS_ENTRY = """
142
+ {country_code}: offers(country: {country_code}, platform: WEB, filter: $filter) {{
143
+ ...TitleOffer
144
+ __typename
145
+ }}
146
+ """
147
+
148
+
149
+ class OfferPackage(NamedTuple):
150
+ """Parsed single offer package from JustWatch GraphQL API for single entry.
151
+ Contains information about platform on which given offer is available."""
152
+
153
+ id: str
154
+ """ID, defines whole platform on which this offer is available, not a single offer."""
155
+
156
+ package_id: int
157
+ """Package ID, defines whole platform on which this offer is available, not a single offer."""
158
+
159
+ name: str
160
+ """Name of the platform in format suited to display for users."""
161
+
162
+ technical_name: str
163
+ """Technical name of the platform, usually all lowercase with no whitespaces."""
164
+
165
+ icon: str
166
+ """Platform icon URL."""
167
+
168
+
169
+ class Offer(NamedTuple):
170
+ """Parsed single offer from JustWatch GraphQL API for single entry.
171
+ One platform can have multiple offers for one entry available, e.g. renting, buying, etc."""
172
+
173
+ id: str
174
+ """Offer ID."""
175
+
176
+ monetization_type: str
177
+ """Type of monetization of this offer, e.g. ``FLATRATE`` (streaming), ``RENT``, ``BUY``."""
178
+
179
+ presentation_type: str
180
+ """Quality of media in this offer, e.g. ``HD``, ``SD``, ``4K``."""
181
+
182
+ price_string: str | None
183
+ """Current price as a string with currency, suitable for displaying to users.
184
+ Format can change based on used ``language`` argument."""
185
+
186
+ price_value: float | None
187
+ """Current price as a numeric value."""
188
+
189
+ price_currency: str
190
+ """Represents only currency, without price, or value."""
191
+
192
+ last_change_retail_price_value: float | None
193
+ """Previous available price if change in price was recorded."""
194
+
195
+ type: str
196
+ """Type of offer."""
197
+
198
+ package: OfferPackage
199
+ """Information about platform on which this offer is available."""
200
+
201
+ url: str
202
+ """URL to this offer."""
203
+
204
+ element_count: int
205
+ """Element count, usually 0."""
206
+
207
+ available_to: str | None
208
+ """Date until which this offer will be available."""
209
+
210
+ deeplink_roku: str | None
211
+ """Deeplink to this offer in Roku."""
212
+
213
+ subtitle_languages: list[str]
214
+ """List of 2-letter language codes of available subtitles, e.g. ``["en", "pt", "de"]``."""
215
+
216
+ video_technology: list[str]
217
+ """List of known video technologies available in this offer, e.g. ``DOLBY_VISION``."""
218
+
219
+ audio_technology: list[str]
220
+ """List of known audio technologies available in this offer, e.g. ``DOLBY_ATMOS``."""
221
+
222
+ audio_languages: list[str]
223
+ """List of 2-letter language codes of available audio tracks, e.g. ``["en", "pt", "de"]``."""
224
+
225
+
226
+ class MediaEntry(NamedTuple):
227
+ """Parsed response from JustWatch GraphQL API for "GetSearchTitles" query for single entry."""
228
+
229
+ entry_id: str
230
+ """Entry ID, contains type code and numeric ID."""
231
+
232
+ object_id: int
233
+ """Object ID, the numeric part of full entry ID."""
234
+
235
+ object_type: str
236
+ """Type of entry, e.g. ``MOVIE``, ``SHOW``."""
237
+
238
+ title: str
239
+ """Full title."""
240
+
241
+ url: str
242
+ """URL to JustWatch with details for this entry."""
243
+
244
+ release_year: int
245
+ """Release year as a number."""
246
+
247
+ release_date: str
248
+ """Full release date as a string, e.g. ``2013-12-16``."""
249
+
250
+ runtime_minutes: int
251
+ """Runtime in minutes."""
252
+
253
+ short_description: str
254
+ """Short description of this entry."""
255
+
256
+ genres: list[str]
257
+ """List of genre codes for this entry, e.g. ``["rly"]``, ``["cmy", "drm", "rma"]``."""
258
+
259
+ imdb_id: str | None
260
+ """ID of this entry in IMDB."""
261
+
262
+ poster: str | None
263
+ """URL to poster for this ID."""
264
+
265
+ backdrops: list[str]
266
+ """List of URLs for backdrops (full screen images to use as background)."""
267
+
268
+ offers: list[Offer]
269
+ """List of available offers for this entry, empty if there are no available offers."""
270
+
271
+
272
+ def prepare_search_request(
273
+ title: str, country: str, language: str, count: int, best_only: bool
274
+ ) -> dict:
275
+ """Prepare search request for JustWatch GraphQL API.
276
+ Creates a ``GetSearchTitles`` GraphQL query.
277
+
278
+ Country code should be two uppercase letters, however it will be auto-converted to uppercase.
279
+
280
+ Meant to be used together with :func:`parse_search_response`.
281
+
282
+ Args:
283
+ title: title to search
284
+ country: country to search for offers
285
+ language: language of responses
286
+ count: how many responses should be returned
287
+ best_only: return only best offers if ``True``, return all offers if ``False``
288
+
289
+ Returns:
290
+ JSON/dict with GraphQL POST body
291
+ """
292
+ _assert_country_code_is_valid(country)
293
+ return {
294
+ "operationName": "GetSearchTitles",
295
+ "variables": {
296
+ "first": count,
297
+ "searchTitlesFilter": {"searchQuery": title},
298
+ "language": language,
299
+ "country": country.upper(),
300
+ "formatPoster": "JPG",
301
+ "formatOfferIcon": "PNG",
302
+ "profile": "S718",
303
+ "backdropProfile": "S1920",
304
+ "filter": {"bestOnly": best_only},
305
+ },
306
+ "query": _GRAPHQL_SEARCH_QUERY + _GRAPHQL_DETAILS_FRAGMENT + _GRAPHQL_OFFER_FRAGMENT,
307
+ }
308
+
309
+
310
+ def parse_search_response(json: dict) -> list[MediaEntry]:
311
+ """Parse response from search query from JustWatch GraphQL API.
312
+ Parses response for ``GetSearchTitles`` query.
313
+
314
+ If API didn't return any data, then an empty list is returned.
315
+
316
+ Meant to be used together with :func:`prepare_search_request`.
317
+
318
+ Args:
319
+ json: JSON returned by JustWatch GraphQL API
320
+
321
+ Returns:
322
+ Parsed received JSON as a list of ``MediaEntry`` NamedTuples
323
+ """
324
+ return [_parse_entry(edge["node"]) for edge in json["data"]["popularTitles"]["edges"]]
325
+
326
+
327
+ def prepare_details_request(node_id: str, country: str, language: str, best_only: bool) -> dict:
328
+ """Prepare a details request for specified node ID to JustWatch GraphQL API.
329
+ Creates a ``GetTitleNode`` GraphQL query.
330
+
331
+ Country code should be two uppercase letters, however it will be auto-converted to uppercase.
332
+
333
+ Meant to be used together with :func:`parse_details_response`.
334
+
335
+ Args:
336
+ node_id: node ID of entry to get details for
337
+ country: country to search for offers
338
+ language: language of responses
339
+ best_only: return only best offers if ``True``, return all offers if ``False``
340
+
341
+ Returns:
342
+ JSON/dict with GraphQL POST body
343
+ """
344
+ _assert_country_code_is_valid(country)
345
+ return {
346
+ "operationName": "GetTitleNode",
347
+ "variables": {
348
+ "nodeId": node_id,
349
+ "language": language,
350
+ "country": country.upper(),
351
+ "formatPoster": "JPG",
352
+ "formatOfferIcon": "PNG",
353
+ "profile": "S718",
354
+ "backdropProfile": "S1920",
355
+ "filter": {"bestOnly": best_only},
356
+ },
357
+ "query": _GRAPHQL_DETAILS_QUERY + _GRAPHQL_DETAILS_FRAGMENT + _GRAPHQL_OFFER_FRAGMENT,
358
+ }
359
+
360
+
361
+ def parse_details_response(json: any) -> MediaEntry | None:
362
+ """Parse response from details query from JustWatch GraphQL API.
363
+ Parses response for ``GetTitleNode`` query.
364
+
365
+ If API responded with an internal error (mostly due to not found node ID),
366
+ then ``None`` will be returned instead.
367
+
368
+ Meant to be used together with :func:`prepare_details_request`.
369
+
370
+ Args:
371
+ json: JSON returned by JustWatch GraphQL API
372
+
373
+ Returns:
374
+ Parsed received JSON as a ``MediaEntry`` NamedTuple,
375
+ or ``None`` in case data for a given node ID was not found
376
+ """
377
+ return _parse_entry(json["data"]["node"]) if "errors" not in json else None
378
+
379
+
380
+ def prepare_offers_for_countries_request(
381
+ node_id: str, countries: set[str], language: str, best_only: bool
382
+ ) -> dict:
383
+ """Prepare an offers request for specified node ID and for all specified countries
384
+ to JustWatch GraphQL API.
385
+ Creates a ``GetTitleOffers`` GraphQL query.
386
+
387
+ Country codes should be two uppercase letters, however they will be auto-converted to uppercase.
388
+ ``countries`` argument mustn't be empty.
389
+
390
+ Meant to be used together with :func:`parse_offers_for_countries_response`.
391
+
392
+ Args:
393
+ node_id: node ID of entry to get details for
394
+ countries: list of country codes to search for offers
395
+ language: language of responses
396
+ best_only: return only best offers if ``True``, return all offers if ``False``
397
+
398
+ Returns:
399
+ JSON/dict with GraphQL POST body
400
+ """
401
+ assert countries, "Cannot prepare offers request without specified countries"
402
+ for country in countries:
403
+ _assert_country_code_is_valid(country)
404
+ return {
405
+ "operationName": "GetTitleOffers",
406
+ "variables": {
407
+ "nodeId": node_id,
408
+ "language": language,
409
+ "formatPoster": "JPG",
410
+ "formatOfferIcon": "PNG",
411
+ "profile": "S718",
412
+ "backdropProfile": "S1920",
413
+ "filter": {"bestOnly": best_only},
414
+ },
415
+ "query": _prepare_offers_for_countries_entry(countries),
416
+ }
417
+
418
+
419
+ def parse_offers_for_countries_response(json: any, countries: set[str]) -> dict[str, list[Offer]]:
420
+ """Parse response from offers query from JustWatch GraphQL API.
421
+ Parses response for ``GetTitleOffers`` query.
422
+
423
+ Response if searched for country codes passed as ``countries`` argument.
424
+ Countries in JSON response which are not present in ``countries`` set will be ignored.
425
+ If response doesn't have offers for a country, then that country still will be present
426
+ in returned dict, just with an empty list as value.
427
+
428
+ Meant to be used together with :func:`prepare_offers_for_countries_request`.
429
+
430
+ Args:
431
+ json: JSON returned by JustWatch GraphQL API
432
+ countries: set of countries to look for in API response
433
+
434
+ Returns:
435
+ A dict, where keys are matching ``countries`` argument and values are offers for a given
436
+ country parsed from JSON response.
437
+ """
438
+ offers_node = json["data"]["node"]
439
+ return {
440
+ country: list(map(_parse_offer, offers_node.get(country.upper(), [])))
441
+ for country in countries
442
+ }
443
+
444
+
445
+ def _assert_country_code_is_valid(code: str) -> None:
446
+ assert len(code) == 2, f"Invalid country code: {code}, code must be 2 characters long"
447
+
448
+
449
+ def _prepare_offers_for_countries_entry(countries: set[str]) -> str:
450
+ offer_requests = [
451
+ _GRAPHQL_COUNTRY_OFFERS_ENTRY.format(country_code=country_code.upper())
452
+ for country_code in countries
453
+ ]
454
+ main_body = _GRAPHQL_OFFERS_BY_COUNTRY_QUERY.format(country_entries="\n".join(offer_requests))
455
+ return main_body + _GRAPHQL_OFFER_FRAGMENT
456
+
457
+
458
+ def _parse_entry(json: any) -> MediaEntry:
459
+ entry_id = json.get("id")
460
+ object_id = json.get("objectId")
461
+ object_type = json.get("objectType")
462
+ content = json["content"]
463
+ title = content.get("title")
464
+ url = _DETAILS_URL + content.get("fullPath")
465
+ year = content.get("originalReleaseYear")
466
+ date = content.get("originalReleaseDate")
467
+ runtime_minutes = content.get("runtime")
468
+ short_description = content.get("shortDescription")
469
+ genres = [node.get("shortName") for node in content.get("genres", []) if node]
470
+ external_ids = content.get("externalIds")
471
+ imdb_id = external_ids.get("imdbId") if external_ids else None
472
+ poster_url_field = content.get("posterUrl")
473
+ poster = _IMAGES_URL + poster_url_field if poster_url_field else None
474
+ backdrops = [_IMAGES_URL + bd.get("backdropUrl") for bd in content.get("backdrops", []) if bd]
475
+ offers = [_parse_offer(offer) for offer in json.get("offers", []) if offer]
476
+ return MediaEntry(
477
+ entry_id,
478
+ object_id,
479
+ object_type,
480
+ title,
481
+ url,
482
+ year,
483
+ date,
484
+ runtime_minutes,
485
+ short_description,
486
+ genres,
487
+ imdb_id,
488
+ poster,
489
+ backdrops,
490
+ offers,
491
+ )
492
+
493
+
494
+ def _parse_offer(json: any) -> Offer:
495
+ id = json.get("id")
496
+ monetization_type = json.get("monetizationType")
497
+ presentation_type = json.get("presentationType")
498
+ price_string = json.get("retailPrice")
499
+ price_value = json.get("retailPriceValue")
500
+ price_currency = json.get("currency")
501
+ last_change_retail_price_value = json.get("lastChangeRetailPriceValue")
502
+ type = json.get("type")
503
+ package = _parse_package(json["package"])
504
+ url = json.get("standardWebURL")
505
+ element_count = json.get("elementCount", 0)
506
+ available_to = json.get("availableTo")
507
+ deeplink_roku = json.get("deeplinkRoku")
508
+ subtitle_languages = json.get("subtitleLanguages")
509
+ video_technology = json.get("videoTechnology")
510
+ audio_technology = json.get("audioTechnology")
511
+ audio_languages = json.get("audioLanguages")
512
+ return Offer(
513
+ id,
514
+ monetization_type,
515
+ presentation_type,
516
+ price_string,
517
+ price_value,
518
+ price_currency,
519
+ last_change_retail_price_value,
520
+ type,
521
+ package,
522
+ url,
523
+ element_count,
524
+ available_to,
525
+ deeplink_roku,
526
+ subtitle_languages,
527
+ video_technology,
528
+ audio_technology,
529
+ audio_languages,
530
+ )
531
+
532
+
533
+ def _parse_package(json: any) -> OfferPackage:
534
+ id = json.get("id")
535
+ package_id = json.get("packageId")
536
+ name = json.get("clearName")
537
+ technical_name = json.get("technicalName")
538
+ icon = _IMAGES_URL + json.get("icon")
539
+ return OfferPackage(id, package_id, name, technical_name, icon)