simple-justwatch-python-api 1.0.3__tar.gz → 1.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple-justwatch-python-api
3
- Version: 1.0.3
3
+ Version: 1.2.0
4
4
  Summary: A simple JustWatch Python API
5
5
  Keywords: justwatch,api,graphql
6
6
  Author: Electronic Mango
@@ -91,8 +91,8 @@ for entry in results:
91
91
 
92
92
  ## License
93
93
 
94
- This library is licensed under **MIT license** (
95
- [LICENSE](https://github.com/Electronic-Mango/simple-justwatch-python-api/blob/main/LICENSE)
94
+ This library is licensed under **MIT license**
95
+ ([LICENSE](https://github.com/Electronic-Mango/simple-justwatch-python-api/blob/main/LICENSE)
96
96
  or <https://opensource.org/license/MIT>).
97
97
 
98
98
 
@@ -63,8 +63,8 @@ for entry in results:
63
63
 
64
64
  ## License
65
65
 
66
- This library is licensed under **MIT license** (
67
- [LICENSE](https://github.com/Electronic-Mango/simple-justwatch-python-api/blob/main/LICENSE)
66
+ This library is licensed under **MIT license**
67
+ ([LICENSE](https://github.com/Electronic-Mango/simple-justwatch-python-api/blob/main/LICENSE)
68
68
  or <https://opensource.org/license/MIT>).
69
69
 
70
70
 
@@ -3,7 +3,7 @@ name = "simple-justwatch-python-api"
3
3
  authors = [
4
4
  { name = "Electronic Mango", email = "78230210+Electronic-Mango@users.noreply.github.com" },
5
5
  ]
6
- version = "1.0.3"
6
+ version = "1.2.0"
7
7
  description = "A simple JustWatch Python API"
8
8
  readme = "README.md"
9
9
  license = "MIT"
@@ -4,14 +4,7 @@ Exceptions raised by this library.
4
4
  All exceptions inherit from [`JustWatchError`]
5
5
  [simplejustwatchapi.exceptions.JustWatchError] for easier catching.
6
6
 
7
- Specific exceptions are raised for non-`2xx` HTTP response status codes and GraphQL
8
- API response errors.
9
-
10
- Each exception includes relevant information for why it was raised, but not always in
11
- any particular parsed format. For example, [`JustWatchApiError`]
12
- [simplejustwatchapi.exceptions.JustWatchApiError] includes the list of errors from the
13
- API response, but stored as a `dict`/JSON, as it was received from the API.
14
-
7
+ Specific exceptions are raised for HTTP-related errors and GraphQL API response errors.
15
8
  """
16
9
 
17
10
 
@@ -23,7 +16,7 @@ class JustWatchApiError(JustWatchError):
23
16
  """
24
17
  Raised when JustWatch API returned errors in JSON response.
25
18
 
26
- If this error is raised, then API responded with status code `2xx`, but there are
19
+ If this error is raised, then no HTTP-related error occurred, but there are
27
20
  listed errors in the internal JSON response. It can happen for too high complexity
28
21
  of request, invalid node ID in functions like [`details`]
29
22
  [simplejustwatchapi.justwatch.details], or invalid country or language codes.
@@ -43,18 +36,19 @@ class JustWatchApiError(JustWatchError):
43
36
 
44
37
  class JustWatchHttpError(JustWatchError):
45
38
  """
46
- Raised when JustWatch API returned a non-`2xx` status code.
39
+ Raised when HTTP-related error occurs.
47
40
 
48
- Any additional verification is not performed, ony the status code is checked.
41
+ This is a general exception for any HTTP-related errors, such as non-`2xx` status
42
+ codes, network errors, timeouts, etc.
49
43
 
50
44
  Attributes:
51
- code (int): HTTP status code returned by the API.
52
- message (str): HTTP message response from the API.
45
+ msg (str): Error message describing the HTTP error.
46
+ response (str | None): Optional text of the HTTP response, if available.
47
+ Usucally contains JSON with error responses from the API.
53
48
 
54
49
  """
55
50
 
56
- def __init__(self, code: int, message: str) -> None:
57
- """Init JustWatchHttpError with status code and message from response."""
58
- super().__init__(f"HTTP code {code}: {message}")
59
- self.code = code
60
- self.message = message
51
+ def __init__(self, msg: str, response: str | None = None) -> None:
52
+ """Init JustWatchHttpError with error message and optional response text."""
53
+ super().__init__(msg)
54
+ self.response = response
@@ -0,0 +1,379 @@
1
+ """
2
+ Module responsible for preparing full GraphQL queries.
3
+
4
+ Queries are usually prepared as main query + needed fragments.
5
+ Specific details are stored as separate GraphQL fragments / Python strings for easier
6
+ reuse and maintainability.
7
+ """
8
+
9
+ _GRAPHQL_SEARCH_QUERY = """
10
+ query GetSearchTitles(
11
+ $searchTitlesFilter: TitleFilter!,
12
+ $country: Country!,
13
+ $language: Language!,
14
+ $first: Int!,
15
+ $formatPoster: ImageFormat,
16
+ $formatOfferIcon: ImageFormat,
17
+ $profile: PosterProfile,
18
+ $backdropProfile: BackdropProfile,
19
+ $filter: OfferFilter!,
20
+ $offset: Int = 0,
21
+ ) {
22
+ popularTitles(
23
+ country: $country
24
+ filter: $searchTitlesFilter
25
+ first: $first
26
+ sortBy: POPULAR
27
+ sortRandomSeed: 0
28
+ offset: $offset
29
+ ) {
30
+ edges {
31
+ node {
32
+ ...TitleDetails
33
+ __typename
34
+ }
35
+ __typename
36
+ }
37
+ __typename
38
+ }
39
+ }
40
+ """
41
+
42
+ _GRAPHQL_POPULAR_QUERY = """
43
+ query GetPopularTitles(
44
+ $popularTitlesFilter: TitleFilter
45
+ $country: Country!
46
+ $language: Language!
47
+ $first: Int! = 70
48
+ $formatPoster: ImageFormat,
49
+ $formatOfferIcon: ImageFormat,
50
+ $profile: PosterProfile
51
+ $backdropProfile: BackdropProfile,
52
+ $filter: OfferFilter!,
53
+ $offset: Int = 0
54
+ ) {
55
+ popularTitles(
56
+ country: $country
57
+ filter: $popularTitlesFilter
58
+ first: $first
59
+ sortBy: POPULAR
60
+ sortRandomSeed: 0
61
+ offset: $offset
62
+ ) {
63
+ __typename
64
+ edges {
65
+ node {
66
+ ...TitleDetails
67
+ __typename
68
+ }
69
+ __typename
70
+ }
71
+ }
72
+ }
73
+ """
74
+
75
+ _GRAPHQL_DETAILS_QUERY = """
76
+ query GetTitleNode(
77
+ $nodeId: ID!,
78
+ $language: Language!,
79
+ $country: Country!,
80
+ $formatPoster: ImageFormat,
81
+ $formatOfferIcon: ImageFormat,
82
+ $profile: PosterProfile,
83
+ $backdropProfile: BackdropProfile,
84
+ $filter: OfferFilter!,
85
+ ) {
86
+ node(id: $nodeId) {
87
+ ...TitleDetails
88
+ __typename
89
+ }
90
+ __typename
91
+ }
92
+ """
93
+
94
+ _GRAPHQL_SEASONS_QUERY = """
95
+ query GetTitleNode(
96
+ $nodeId: ID!,
97
+ $language: Language!,
98
+ $country: Country!,
99
+ $formatPoster: ImageFormat,
100
+ $formatOfferIcon: ImageFormat,
101
+ $profile: PosterProfile,
102
+ $backdropProfile: BackdropProfile,
103
+ $filter: OfferFilter!,
104
+ ) {
105
+ node(id: $nodeId) {
106
+ ... on Show {
107
+ seasons(sortDirection: ASC) {
108
+ ...TitleDetails
109
+ }
110
+ }
111
+ __typename
112
+ }
113
+ __typename
114
+ }
115
+ """
116
+
117
+ _GRAPHQL_EPISODES_QUERY = """
118
+ query GetTitleNode(
119
+ $nodeId: ID!,
120
+ $language: Language!,
121
+ $country: Country!,
122
+ $formatPoster: ImageFormat,
123
+ $formatOfferIcon: ImageFormat,
124
+ $profile: PosterProfile,
125
+ $backdropProfile: BackdropProfile,
126
+ $filter: OfferFilter!,
127
+ ) {
128
+ node(id: $nodeId) {
129
+ ... on Season {
130
+ episodes(sortDirection: ASC) {
131
+ ...TitleDetails
132
+ }
133
+ }
134
+ __typename
135
+ }
136
+ __typename
137
+ }
138
+ """
139
+
140
+ _GRAPHQL_PROVIDERS_QUERY = """
141
+ query GetProviders(
142
+ $country: Country!,
143
+ $formatOfferIcon: ImageFormat
144
+ ) {
145
+ packages(
146
+ country: $country
147
+ platform: WEB
148
+ includeAddons: true
149
+ ) {
150
+ ...PackageDetails
151
+ }
152
+ __typename
153
+ }
154
+ """
155
+
156
+ _GRAPHQL_OFFERS_BY_COUNTRY_QUERY = """
157
+ query GetTitleOffers(
158
+ $nodeId: ID!,
159
+ $language: Language!,
160
+ $formatOfferIcon: ImageFormat,
161
+ $filter: OfferFilter!,
162
+ ) {{
163
+ node(id: $nodeId) {{
164
+ ... on MovieOrShowOrSeasonOrEpisode {{
165
+ {country_entries}
166
+ __typename
167
+ }}
168
+ __typename
169
+ }}
170
+ __typename
171
+ }}
172
+ """
173
+
174
+ _GRAPHQL_DETAILS_FRAGMENT = """
175
+ fragment TitleDetails on MovieOrShowOrSeasonOrEpisode {
176
+ id
177
+ objectId
178
+ objectType
179
+ content(country: $country, language: $language) {
180
+ ...ContentDetails
181
+ __typename
182
+ }
183
+ ...StreamingChartInfoFragment
184
+ ... on Show {
185
+ totalSeasonCount
186
+ }
187
+ ... on Season {
188
+ totalEpisodeCount
189
+ }
190
+ offers(country: $country, platform: WEB, filter: $filter) {
191
+ ...TitleOffer
192
+ }
193
+ __typename
194
+ }
195
+
196
+ fragment StreamingChartInfoFragment on MovieOrShowOrSeason {
197
+ streamingCharts(country: $country) {
198
+ edges {
199
+ streamingChartInfo {
200
+ rank
201
+ trend
202
+ trendDifference
203
+ daysInTop3
204
+ daysInTop10
205
+ daysInTop100
206
+ daysInTop1000
207
+ topRank
208
+ updatedAt
209
+ __typename
210
+ }
211
+ __typename
212
+ }
213
+ __typename
214
+ }
215
+ }
216
+
217
+ fragment ContentDetails on MovieOrShowOrSeasonOrEpisodeContent {
218
+ title
219
+ originalReleaseYear
220
+ originalReleaseDate
221
+ runtime
222
+ shortDescription
223
+ ...FullContentDetails
224
+ ... on MovieOrShowContent {
225
+ ageCertification
226
+ }
227
+ ... on SeasonContent {
228
+ seasonNumber
229
+ }
230
+ ... on EpisodeContent {
231
+ seasonNumber
232
+ episodeNumber
233
+ }
234
+ }
235
+
236
+ fragment FullContentDetails on MovieOrShowOrSeasonContent {
237
+ fullPath
238
+ genres {
239
+ shortName
240
+ __typename
241
+ }
242
+ externalIds {
243
+ imdbId
244
+ tmdbId
245
+ __typename
246
+ }
247
+ posterUrl(profile: $profile, format: $formatPoster)
248
+ backdrops(profile: $backdropProfile, format: $formatPoster) {
249
+ backdropUrl
250
+ __typename
251
+ }
252
+ scoring {
253
+ imdbScore
254
+ imdbVotes
255
+ tmdbPopularity
256
+ tmdbScore
257
+ tomatoMeter
258
+ certifiedFresh
259
+ jwRating
260
+ __typename
261
+ }
262
+ interactions {
263
+ likelistAdditions
264
+ dislikelistAdditions
265
+ __typename
266
+ }
267
+ }
268
+ """
269
+
270
+ _GRAPHQL_OFFER_FRAGMENT = """
271
+ fragment TitleOffer on Offer {
272
+ id
273
+ monetizationType
274
+ presentationType
275
+ retailPrice(language: $language)
276
+ retailPriceValue
277
+ currency
278
+ lastChangeRetailPriceValue
279
+ type
280
+ package {
281
+ ...PackageDetails
282
+ }
283
+ standardWebURL
284
+ elementCount
285
+ availableTo
286
+ deeplinkRoku: deeplinkURL(platform: ROKU_OS)
287
+ subtitleLanguages
288
+ videoTechnology
289
+ audioTechnology
290
+ audioLanguages
291
+ __typename
292
+ }
293
+ """
294
+
295
+ _GRAPHQL_PACKAGE_FRAGMENT = """
296
+ fragment PackageDetails on Package {
297
+ id
298
+ packageId
299
+ clearName
300
+ technicalName
301
+ shortName
302
+ slug
303
+ monetizationTypes
304
+ icon(profile: S100, format: $formatOfferIcon)
305
+ __typename
306
+ }
307
+ """
308
+
309
+ _GRAPHQL_COUNTRY_OFFERS_ENTRY = """
310
+ {country_code}: offers(country: {country_code}, platform: WEB, filter: $filter) {{
311
+ ...TitleOffer
312
+ __typename
313
+ }}
314
+ """
315
+
316
+ GRAPHQL_SEARCH_QUERY = (
317
+ _GRAPHQL_SEARCH_QUERY
318
+ + _GRAPHQL_DETAILS_FRAGMENT
319
+ + _GRAPHQL_OFFER_FRAGMENT
320
+ + _GRAPHQL_PACKAGE_FRAGMENT
321
+ )
322
+
323
+ GRAPHQL_POPULAR_QUERY = (
324
+ _GRAPHQL_POPULAR_QUERY
325
+ + _GRAPHQL_DETAILS_FRAGMENT
326
+ + _GRAPHQL_OFFER_FRAGMENT
327
+ + _GRAPHQL_PACKAGE_FRAGMENT
328
+ )
329
+
330
+ GRAPHQL_PROVIDERS_QUERY = _GRAPHQL_PROVIDERS_QUERY + _GRAPHQL_PACKAGE_FRAGMENT
331
+
332
+ GRAPHQL_DETAILS_QUERY = (
333
+ _GRAPHQL_DETAILS_QUERY
334
+ + _GRAPHQL_DETAILS_FRAGMENT
335
+ + _GRAPHQL_OFFER_FRAGMENT
336
+ + _GRAPHQL_PACKAGE_FRAGMENT
337
+ )
338
+
339
+ GRAPHQL_SEASONS_QUERY = (
340
+ _GRAPHQL_SEASONS_QUERY
341
+ + _GRAPHQL_DETAILS_FRAGMENT
342
+ + _GRAPHQL_OFFER_FRAGMENT
343
+ + _GRAPHQL_PACKAGE_FRAGMENT
344
+ )
345
+
346
+ GRAPHQL_EPISODES_QUERY = (
347
+ _GRAPHQL_EPISODES_QUERY
348
+ + _GRAPHQL_DETAILS_FRAGMENT
349
+ + _GRAPHQL_OFFER_FRAGMENT
350
+ + _GRAPHQL_PACKAGE_FRAGMENT
351
+ )
352
+
353
+
354
+ def graphql_offers_for_countries_query(countries: set[str]) -> str:
355
+ """
356
+ Prepare GraphQL query with a list of offers from specified countries.
357
+
358
+ The full query is `GetTitleOffers` query with a list of offers per country.
359
+ No additional information is returned, only offers.
360
+ Can be used for all entry types - movies, shows, seasons, episodes.
361
+
362
+ The input is a set of 2-letter country codes. This function assumes that codes are
363
+ valid length and the set is not empty; it performs no verification on its own.
364
+
365
+ Args:
366
+ countries (set[str]): 2-letter country codes.
367
+
368
+ Returns:
369
+ (str): GraphQL `GetTitleOffers` query with available offers per country code.
370
+
371
+ """
372
+ offer_requests = [
373
+ _GRAPHQL_COUNTRY_OFFERS_ENTRY.format(country_code=country_code.upper())
374
+ for country_code in countries
375
+ ]
376
+ main_query = _GRAPHQL_OFFERS_BY_COUNTRY_QUERY.format(
377
+ country_entries="\n".join(offer_requests)
378
+ )
379
+ return main_query + _GRAPHQL_OFFER_FRAGMENT + _GRAPHQL_PACKAGE_FRAGMENT
@@ -41,13 +41,13 @@ Each function can raise two exceptions:
41
41
  | Exception | Cause |
42
42
  |-----------|-------|
43
43
  | [`JustWatchHttpError`][simplejustwatchapi.exceptions.JustWatchHttpError] | \
44
- JustWatch API responded with non-`2xx` code. |
44
+ HTTP error occurred, e.g., JustWatch API responded with non-`2xx` status code. |
45
45
  | [`JustWatchApiError`][simplejustwatchapi.exceptions.JustWatchApiError] | \
46
- JSON response from JustWatch API contains errors (e.g., due to invalid language or \
47
- country code). If this exception is raised, then API responded with `2xx` code. |
46
+ JSON response from JustWatch API contains errors, e.g., due to invalid language or \
47
+ country code. |
48
48
  """
49
49
 
50
- from httpx import post
50
+ from httpx import HTTPError, HTTPStatusError, post
51
51
 
52
52
  from simplejustwatchapi.exceptions import JustWatchHttpError
53
53
  from simplejustwatchapi.query import (
@@ -154,7 +154,8 @@ def search(
154
154
  Raises:
155
155
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
156
156
  due to invalid language or country code.
157
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
157
+ exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
158
+ responded with non-`2xx` status code.
158
159
 
159
160
  """
160
161
  request = prepare_search_request(
@@ -238,7 +239,8 @@ def popular(
238
239
  Raises:
239
240
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
240
241
  due to invalid language or country code.
241
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
242
+ exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
243
+ responded with non-`2xx` status code.
242
244
 
243
245
  """
244
246
  request = prepare_popular_request(
@@ -307,7 +309,8 @@ def details(
307
309
  Raises:
308
310
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
309
311
  due to invalid language or country code.
310
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
312
+ exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
313
+ responded with non-`2xx` status code.
311
314
 
312
315
  """
313
316
  request = prepare_details_request(node_id, country, language, best_only)
@@ -355,7 +358,8 @@ def seasons(
355
358
  Raises:
356
359
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
357
360
  due to invalid language or country code.
358
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
361
+ exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
362
+ responded with non-`2xx` status code.
359
363
 
360
364
  """
361
365
  request = prepare_seasons_request(show_id, country, language, best_only)
@@ -404,7 +408,8 @@ def episodes(
404
408
  Raises:
405
409
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
406
410
  due to invalid language or country code.
407
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
411
+ exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
412
+ responded with non-`2xx` status code.
408
413
 
409
414
  """
410
415
  request = prepare_episodes_request(season_id, country, language, best_only)
@@ -472,7 +477,8 @@ def offers_for_countries(
472
477
  Raises:
473
478
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
474
479
  due to invalid language or country code.
475
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
480
+ exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
481
+ responded with non-`2xx` status code.
476
482
 
477
483
  """
478
484
  if not countries:
@@ -506,7 +512,8 @@ def providers(country: str = "US") -> list[OfferPackage]:
506
512
  Raises:
507
513
  exceptions.JustWatchApiError: JSON response from API has internal errors, e.g.,
508
514
  due to invalid language or country code.
509
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
515
+ exceptions.JustWatchHttpError: HTTP error occurred, e.g., JustWatch API
516
+ responded with non-`2xx` status code.
510
517
 
511
518
  """
512
519
  request = prepare_providers_request(country)
@@ -525,10 +532,14 @@ def _post_to_jw_graphql_api(request_json: dict) -> dict:
525
532
  (dict): JSON response from the API.
526
533
 
527
534
  Raises:
528
- exceptions.JustWatchHttpError: JustWatch API didn't respond with `2xx` code.
535
+ exceptions.JustWatchHttpError: HTTP-related error occurred.
529
536
 
530
537
  """
531
- response = post(_GRAPHQL_API_URL, json=request_json)
532
- if not response.is_success:
533
- raise JustWatchHttpError(response.status_code, response.text)
534
- return response.json()
538
+ try:
539
+ response = post(_GRAPHQL_API_URL, json=request_json)
540
+ response.raise_for_status()
541
+ return response.json()
542
+ except HTTPStatusError as e:
543
+ raise JustWatchHttpError(str(e), e.response.text) from e
544
+ except HTTPError as e:
545
+ raise JustWatchHttpError(str(e)) from e
@@ -4,7 +4,7 @@ Module responsible for creating requests to and parsing responses from JustWatch
4
4
  Functions are prepared in pairs - prepare request and parse response for specific
5
5
  operation. "Request" functions do no verification of input data; "parse" functions check
6
6
  if returned JSON/`dict` contain `error` key. In such case a [`JustWatchApiError`]
7
- [simplejustwatchapi.erxceptions.JustWatchApiError] is raised.
7
+ [simplejustwatchapi.exceptions.JustWatchApiError] is raised.
8
8
 
9
9
  All "parse" functions convert JSON returned by API into request-specific Python
10
10
  [`NamedTuple`][typing.NamedTuple].
@@ -14,13 +14,13 @@ from typing import Any
14
14
 
15
15
  from simplejustwatchapi.exceptions import JustWatchApiError, JustWatchError
16
16
  from simplejustwatchapi.graphql import (
17
- graphql_details_query,
18
- graphql_episodes_query,
17
+ GRAPHQL_DETAILS_QUERY,
18
+ GRAPHQL_EPISODES_QUERY,
19
+ GRAPHQL_POPULAR_QUERY,
20
+ GRAPHQL_PROVIDERS_QUERY,
21
+ GRAPHQL_SEARCH_QUERY,
22
+ GRAPHQL_SEASONS_QUERY,
19
23
  graphql_offers_for_countries_query,
20
- graphql_popular_query,
21
- graphql_providers_query,
22
- graphql_search_query,
23
- graphql_seasons_query,
24
24
  )
25
25
  from simplejustwatchapi.tuples import (
26
26
  Episode,
@@ -84,7 +84,7 @@ def prepare_search_request(
84
84
  **_locale_variables(country, language),
85
85
  "offset": offset or None,
86
86
  },
87
- "query": graphql_search_query(),
87
+ "query": GRAPHQL_SEARCH_QUERY,
88
88
  }
89
89
 
90
90
 
@@ -157,7 +157,7 @@ def prepare_popular_request(
157
157
  **_locale_variables(country, language),
158
158
  "offset": offset or None,
159
159
  },
160
- "query": graphql_popular_query(),
160
+ "query": GRAPHQL_POPULAR_QUERY,
161
161
  }
162
162
 
163
163
 
@@ -220,7 +220,7 @@ def prepare_details_request(
220
220
  **_common_variables(best_only),
221
221
  **_locale_variables(country, language),
222
222
  },
223
- "query": graphql_details_query(),
223
+ "query": GRAPHQL_DETAILS_QUERY,
224
224
  }
225
225
 
226
226
 
@@ -279,7 +279,7 @@ def prepare_seasons_request(
279
279
  **_common_variables(best_only),
280
280
  **_locale_variables(country, language),
281
281
  },
282
- "query": graphql_seasons_query(),
282
+ "query": GRAPHQL_SEASONS_QUERY,
283
283
  }
284
284
 
285
285
 
@@ -338,7 +338,7 @@ def prepare_episodes_request(
338
338
  **_common_variables(best_only),
339
339
  **_locale_variables(country, language),
340
340
  },
341
- "query": graphql_episodes_query(),
341
+ "query": GRAPHQL_EPISODES_QUERY,
342
342
  }
343
343
 
344
344
 
@@ -469,7 +469,7 @@ def prepare_providers_request(country: str) -> dict[str, Any]:
469
469
  "country": country.upper(),
470
470
  "formatOfferIcon": "PNG",
471
471
  },
472
- "query": graphql_providers_query(),
472
+ "query": GRAPHQL_PROVIDERS_QUERY,
473
473
  }
474
474
 
475
475
 
@@ -711,5 +711,14 @@ def _parse_package(json: Any) -> OfferPackage:
711
711
  name = json.get("clearName")
712
712
  technical_name = json.get("technicalName")
713
713
  short_name = json.get("shortName")
714
+ monetization_types = json.get("monetizationTypes", [])
714
715
  icon = _IMAGES_URL + icon if (icon := json.get("icon")) else ""
715
- return OfferPackage(platform_id, package_id, name, technical_name, short_name, icon)
716
+ return OfferPackage(
717
+ platform_id,
718
+ package_id,
719
+ name,
720
+ technical_name,
721
+ short_name,
722
+ monetization_types,
723
+ icon,
724
+ )
@@ -24,12 +24,14 @@ class OfferPackage(NamedTuple):
24
24
  function to return data about all available providers.
25
25
 
26
26
  Attributes:
27
- id (str): ID of the provider/plaform for this offer.
27
+ id (str): ID of the provider/platform for this offer.
28
28
  package_id (int): Package ID. I'm not sure how it's different from regular `id`.
29
29
  name (str): Name of the platform in format suited to display for users.
30
30
  technical_name (str): Technical name of the platform,
31
31
  usually all lowercase with no whitespaces.
32
32
  short_name (str): 3-letter provider name.
33
+ monetization_types (list[str]): List of monetization types available for this
34
+ provider (e.g., `ADS`, `FLATRATE` (streaming), `RENT`).
33
35
  icon (str): Platform icon URL.
34
36
 
35
37
  """
@@ -39,6 +41,7 @@ class OfferPackage(NamedTuple):
39
41
  name: str
40
42
  technical_name: str
41
43
  short_name: str
44
+ monetization_types: list[str]
42
45
  icon: str
43
46
 
44
47
 
@@ -1,492 +0,0 @@
1
- """
2
- Module responsible for preparing full GraphQL queries.
3
-
4
- Queries are usually prepared as main query + needed fragments.
5
- Specific details are stored as separate GraphQL fragments / Python strings for easier
6
- reuse and maintainability.
7
-
8
- In the long term these queries should be moved to dedicated GraphQL resource files
9
- to allow for formatting and syntax checking. However, the functions used for
10
- constructing full queries shouldn't change.
11
- """
12
-
13
- # TODO: Convert these strings into resources, e.g.,:
14
- # https://docs.python.org/3/library/importlib.resources.html
15
-
16
- _GRAPHQL_SEARCH_QUERY = """
17
- query GetSearchTitles(
18
- $searchTitlesFilter: TitleFilter!,
19
- $country: Country!,
20
- $language: Language!,
21
- $first: Int!,
22
- $formatPoster: ImageFormat,
23
- $formatOfferIcon: ImageFormat,
24
- $profile: PosterProfile,
25
- $backdropProfile: BackdropProfile,
26
- $filter: OfferFilter!,
27
- $offset: Int = 0,
28
- ) {
29
- popularTitles(
30
- country: $country
31
- filter: $searchTitlesFilter
32
- first: $first
33
- sortBy: POPULAR
34
- sortRandomSeed: 0
35
- offset: $offset
36
- ) {
37
- edges {
38
- node {
39
- ...TitleDetails
40
- __typename
41
- }
42
- __typename
43
- }
44
- __typename
45
- }
46
- }
47
- """
48
-
49
- _GRAPHQL_POPULAR_QUERY = """
50
- query GetPopularTitles(
51
- $popularTitlesFilter: TitleFilter
52
- $country: Country!
53
- $language: Language!
54
- $first: Int! = 70
55
- $formatPoster: ImageFormat,
56
- $formatOfferIcon: ImageFormat,
57
- $profile: PosterProfile
58
- $backdropProfile: BackdropProfile,
59
- $filter: OfferFilter!,
60
- $offset: Int = 0
61
- ) {
62
- popularTitles(
63
- country: $country
64
- filter: $popularTitlesFilter
65
- first: $first
66
- sortBy: POPULAR
67
- sortRandomSeed: 0
68
- offset: $offset
69
- ) {
70
- __typename
71
- edges {
72
- node {
73
- ...TitleDetails
74
- __typename
75
- }
76
- __typename
77
- }
78
- }
79
- }
80
- """
81
-
82
- _GRAPHQL_DETAILS_QUERY = """
83
- query GetTitleNode(
84
- $nodeId: ID!,
85
- $language: Language!,
86
- $country: Country!,
87
- $formatPoster: ImageFormat,
88
- $formatOfferIcon: ImageFormat,
89
- $profile: PosterProfile,
90
- $backdropProfile: BackdropProfile,
91
- $filter: OfferFilter!,
92
- ) {
93
- node(id: $nodeId) {
94
- ...TitleDetails
95
- __typename
96
- }
97
- __typename
98
- }
99
- """
100
-
101
- _GRAPHQL_SEASONS_QUERY = """
102
- query GetTitleNode(
103
- $nodeId: ID!,
104
- $language: Language!,
105
- $country: Country!,
106
- $formatPoster: ImageFormat,
107
- $formatOfferIcon: ImageFormat,
108
- $profile: PosterProfile,
109
- $backdropProfile: BackdropProfile,
110
- $filter: OfferFilter!,
111
- ) {
112
- node(id: $nodeId) {
113
- ...on Show {
114
- seasons(sortDirection: ASC) {
115
- ...TitleDetails
116
- }
117
- }
118
- __typename
119
- }
120
- __typename
121
- }
122
- """
123
-
124
- _GRAPHQL_EPISODES_QUERY = """
125
- query GetTitleNode(
126
- $nodeId: ID!,
127
- $language: Language!,
128
- $country: Country!,
129
- $formatPoster: ImageFormat,
130
- $formatOfferIcon: ImageFormat,
131
- $profile: PosterProfile,
132
- $backdropProfile: BackdropProfile,
133
- $filter: OfferFilter!,
134
- ) {
135
- node(id: $nodeId) {
136
- ...on Season {
137
- episodes(sortDirection: ASC) {
138
- ...TitleDetails
139
- }
140
- }
141
- __typename
142
- }
143
- __typename
144
- }
145
- """
146
-
147
- _GRAPHQL_PROVIDERS_QUERY = """
148
- query GetProviders(
149
- $country: Country!,
150
- $formatOfferIcon: ImageFormat
151
- ) {
152
- packages(
153
- country: $country
154
- platform: WEB
155
- includeAddons: true
156
- ) {
157
- ... PackageDetails
158
- }
159
- __typename
160
- }
161
- """
162
-
163
- _GRAPHQL_OFFERS_BY_COUNTRY_QUERY = """
164
- query GetTitleOffers(
165
- $nodeId: ID!,
166
- $language: Language!,
167
- $formatOfferIcon: ImageFormat,
168
- $filter: OfferFilter!,
169
- ) {{
170
- node(id: $nodeId) {{
171
- ... on MovieOrShowOrSeasonOrEpisode {{
172
- {country_entries}
173
- __typename
174
- }}
175
- __typename
176
- }}
177
- __typename
178
- }}
179
- """
180
-
181
- _GRAPHQL_DETAILS_FRAGMENT = """
182
- fragment TitleDetails on MovieOrShowOrSeasonOrEpisode {
183
- id
184
- objectId
185
- objectType
186
- content(country: $country, language: $language) {
187
- ...ContentDetails
188
- __typename
189
- }
190
- ...StreamingChartInfoFragment
191
- ...on Show {
192
- totalSeasonCount
193
- }
194
- ...on Season {
195
- totalEpisodeCount
196
- }
197
- offers(country: $country, platform: WEB, filter: $filter) {
198
- ...TitleOffer
199
- }
200
- __typename
201
- }
202
-
203
- fragment StreamingChartInfoFragment on MovieOrShowOrSeason {
204
- streamingCharts(country: $country) {
205
- edges {
206
- streamingChartInfo {
207
- rank
208
- trend
209
- trendDifference
210
- daysInTop3
211
- daysInTop10
212
- daysInTop100
213
- daysInTop1000
214
- topRank
215
- updatedAt
216
- __typename
217
- }
218
- __typename
219
- }
220
- __typename
221
- }
222
- }
223
-
224
- fragment ContentDetails on MovieOrShowOrSeasonOrEpisodeContent {
225
- title
226
- originalReleaseYear
227
- originalReleaseDate
228
- runtime
229
- shortDescription
230
- ...FullContentDetails
231
- ...on MovieOrShowContent {
232
- ageCertification
233
- }
234
- ...on SeasonContent {
235
- seasonNumber
236
- }
237
- ...on EpisodeContent {
238
- seasonNumber
239
- episodeNumber
240
- }
241
- }
242
-
243
- fragment FullContentDetails on MovieOrShowOrSeasonContent {
244
- fullPath
245
- genres {
246
- shortName
247
- __typename
248
- }
249
- externalIds {
250
- imdbId
251
- tmdbId
252
- __typename
253
- }
254
- posterUrl(profile: $profile, format: $formatPoster)
255
- backdrops(profile: $backdropProfile, format: $formatPoster) {
256
- backdropUrl
257
- __typename
258
- }
259
- scoring {
260
- imdbScore
261
- imdbVotes
262
- tmdbPopularity
263
- tmdbScore
264
- tomatoMeter
265
- certifiedFresh
266
- jwRating
267
- __typename
268
- }
269
- interactions {
270
- likelistAdditions
271
- dislikelistAdditions
272
- __typename
273
- }
274
- }
275
- """
276
-
277
- _GRAPHQL_OFFER_FRAGMENT = """
278
- fragment TitleOffer on Offer {
279
- id
280
- monetizationType
281
- presentationType
282
- retailPrice(language: $language)
283
- retailPriceValue
284
- currency
285
- lastChangeRetailPriceValue
286
- type
287
- package {
288
- ... PackageDetails
289
- }
290
- standardWebURL
291
- elementCount
292
- availableTo
293
- deeplinkRoku: deeplinkURL(platform: ROKU_OS)
294
- subtitleLanguages
295
- videoTechnology
296
- audioTechnology
297
- audioLanguages
298
- __typename
299
- }
300
- """
301
-
302
- _GRAPHQL_PACKAGE_FRAGMENT = """
303
- fragment PackageDetails on Package {
304
- id
305
- packageId
306
- clearName
307
- technicalName
308
- shortName
309
- slug
310
- icon(profile: S100, format: $formatOfferIcon)
311
- __typename
312
- }
313
- """
314
-
315
- _GRAPHQL_COUNTRY_OFFERS_ENTRY = """
316
- {country_code}: offers(country: {country_code}, platform: WEB, filter: $filter) {{
317
- ...TitleOffer
318
- __typename
319
- }}
320
- """
321
-
322
-
323
- def graphql_search_query() -> str:
324
- """
325
- Prepare GraphQL query used for searching for entries.
326
-
327
- The full query is:
328
-
329
- - `GetSearchTitles` query
330
- - `TitleDetails` fragment
331
- - `TitleOffer` fragment
332
- - `PackageDetails` fragment
333
-
334
- Returns:
335
- (str): Full GraphQL `GetSearchTitles` query.
336
-
337
- """
338
- return (
339
- _GRAPHQL_SEARCH_QUERY
340
- + _GRAPHQL_DETAILS_FRAGMENT
341
- + _GRAPHQL_OFFER_FRAGMENT
342
- + _GRAPHQL_PACKAGE_FRAGMENT
343
- )
344
-
345
-
346
- def graphql_popular_query() -> str:
347
- """
348
- Prepare GraphQL query used for looking up currently popular titles.
349
-
350
- The full query is:
351
-
352
- - `GetPopularTitles` query
353
- - `TitleDetails` fragment
354
- - `TitleOffer` fragment
355
- - `PackageDetails` fragment
356
-
357
- Returns:
358
- (str): Full GraphQL `GetPopularTitles` query.
359
-
360
- """
361
- return (
362
- _GRAPHQL_POPULAR_QUERY
363
- + _GRAPHQL_DETAILS_FRAGMENT
364
- + _GRAPHQL_OFFER_FRAGMENT
365
- + _GRAPHQL_PACKAGE_FRAGMENT
366
- )
367
-
368
-
369
- def graphql_providers_query() -> str:
370
- """
371
- Prepare GraphQL query used for looking up all providers for a given country.
372
-
373
- The full query is:
374
-
375
- - `GetProviders` query
376
- - `PackageDetails` fragment
377
-
378
- Returns:
379
- (str): Full GraphQL `GetProviders` query.
380
-
381
- """
382
- return _GRAPHQL_PROVIDERS_QUERY + _GRAPHQL_PACKAGE_FRAGMENT
383
-
384
-
385
- def graphql_details_query() -> str:
386
- """
387
- Prepare GraphQL query used for getting details regarding a single entry.
388
-
389
- The full query is:
390
-
391
- - `GetTitleNode` query
392
- - `TitleDetails` fragment
393
- - `TitleOffer` fragment
394
- - `PackageDetails` fragment
395
-
396
- It is meant for movies and shows, but can be used for seasons and episodes as well,
397
- it just won't return full season/episodes list.
398
-
399
- Returns:
400
- (str): Full GraphQL `GetTitleNode` query.
401
-
402
- """
403
- return (
404
- _GRAPHQL_DETAILS_QUERY
405
- + _GRAPHQL_DETAILS_FRAGMENT
406
- + _GRAPHQL_OFFER_FRAGMENT
407
- + _GRAPHQL_PACKAGE_FRAGMENT
408
- )
409
-
410
-
411
- def graphql_seasons_query() -> str:
412
- """
413
- Prepare GraphQL query used for getting a list of seasons for a single show.
414
-
415
- The full query is:
416
-
417
- - `GetTitleNode` query, but only with a list of seasons
418
- - `TitleDetails` fragment
419
- - `TitleOffer` fragment
420
- - `PackageDetails` fragment
421
-
422
- It will only return data for shows with a list of all available seasons, ascending.
423
- `TitleDetails` query itself matches [`graphql_details_query`]
424
- [simplejustwatchapi.graphql.graphql_details_query], its conditions will return all
425
- relevant data for seasons.
426
-
427
- Returns:
428
- (str): GraphQL `GetTitleNode` query with a list of seasons.
429
-
430
- """
431
- return (
432
- _GRAPHQL_SEASONS_QUERY
433
- + _GRAPHQL_DETAILS_FRAGMENT
434
- + _GRAPHQL_OFFER_FRAGMENT
435
- + _GRAPHQL_PACKAGE_FRAGMENT
436
- )
437
-
438
-
439
- def graphql_episodes_query() -> str:
440
- """
441
- Prepare GraphQL query used for getting a list of episodes for a single show season.
442
-
443
- The full query is:
444
-
445
- - `GetTitleNode` query, but only with a list of episodes
446
- - `TitleDetails` fragment
447
- - `TitleOffer` fragment
448
- - `PackageDetails` fragment
449
-
450
- It will only return data for show seasons with a list of all available episodes,
451
- ascending. `TitleDetails` query itself matches [`graphql_details_query`]
452
- [simplejustwatchapi.graphql.graphql_details_query], its conditions will return all
453
- relevant data for episodes.
454
-
455
- Returns:
456
- (str): GraphQL `GetTitleNode` query with a list of episods.
457
-
458
- """
459
- return (
460
- _GRAPHQL_EPISODES_QUERY
461
- + _GRAPHQL_DETAILS_FRAGMENT
462
- + _GRAPHQL_OFFER_FRAGMENT
463
- + _GRAPHQL_PACKAGE_FRAGMENT
464
- )
465
-
466
-
467
- def graphql_offers_for_countries_query(countries: set[str]) -> str:
468
- """
469
- Prepare GraphQL query with a list of offers from specified countries.
470
-
471
- The full query is `GetTitleOffers` query with a list of offers per country.
472
- No additional information is returned, only offers.
473
- Can be used for all entry types - movies, shows, seasons, episodes.
474
-
475
- The input is a set of 2-letter country codes. This function assumes that codes are
476
- valid length and the set is not empty; it performs no verification on its own.
477
-
478
- Args:
479
- countries (set[str]): 2-letter country codes.
480
-
481
- Returns:
482
- (str): GraphQL `GetTitleOffers` query with available offers per country code.
483
-
484
- """
485
- offer_requests = [
486
- _GRAPHQL_COUNTRY_OFFERS_ENTRY.format(country_code=country_code.upper())
487
- for country_code in countries
488
- ]
489
- main_query = _GRAPHQL_OFFERS_BY_COUNTRY_QUERY.format(
490
- country_entries="\n".join(offer_requests)
491
- )
492
- return main_query + _GRAPHQL_OFFER_FRAGMENT + _GRAPHQL_PACKAGE_FRAGMENT