simple-justwatch-python-api 1.0.2__tar.gz → 1.1.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.
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/PKG-INFO +6 -4
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/README.md +5 -3
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/pyproject.toml +1 -1
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/src/simplejustwatchapi/exceptions.py +5 -22
- simple_justwatch_python_api-1.1.0/src/simplejustwatchapi/graphql.py +379 -0
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/src/simplejustwatchapi/justwatch.py +24 -15
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/src/simplejustwatchapi/query.py +23 -14
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/src/simplejustwatchapi/tuples.py +4 -1
- simple_justwatch_python_api-1.0.2/src/simplejustwatchapi/graphql.py +0 -492
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/LICENSE +0 -0
- {simple_justwatch_python_api-1.0.2 → simple_justwatch_python_api-1.1.0}/src/simplejustwatchapi/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple-justwatch-python-api
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A simple JustWatch Python API
|
|
5
5
|
Keywords: justwatch,api,graphql
|
|
6
6
|
Author: Electronic Mango
|
|
@@ -72,7 +72,8 @@ This Python library has multiple functions:
|
|
|
72
72
|
offers in multiple countries
|
|
73
73
|
- `providers` - get data about available providers (e.g., Netflix)
|
|
74
74
|
|
|
75
|
-
Example outputs from all functions are in
|
|
75
|
+
Example outputs from all functions are in
|
|
76
|
+
[`examples/`](https://github.com/Electronic-Mango/simple-justwatch-python-api/tree/main/examples).
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
|
|
@@ -90,8 +91,9 @@ for entry in results:
|
|
|
90
91
|
|
|
91
92
|
## License
|
|
92
93
|
|
|
93
|
-
This library is licensed under **MIT license**
|
|
94
|
-
|
|
94
|
+
This library is licensed under **MIT license**
|
|
95
|
+
([LICENSE](https://github.com/Electronic-Mango/simple-justwatch-python-api/blob/main/LICENSE)
|
|
96
|
+
or <https://opensource.org/license/MIT>).
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
|
|
@@ -44,7 +44,8 @@ This Python library has multiple functions:
|
|
|
44
44
|
offers in multiple countries
|
|
45
45
|
- `providers` - get data about available providers (e.g., Netflix)
|
|
46
46
|
|
|
47
|
-
Example outputs from all functions are in
|
|
47
|
+
Example outputs from all functions are in
|
|
48
|
+
[`examples/`](https://github.com/Electronic-Mango/simple-justwatch-python-api/tree/main/examples).
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
|
|
@@ -62,8 +63,9 @@ for entry in results:
|
|
|
62
63
|
|
|
63
64
|
## License
|
|
64
65
|
|
|
65
|
-
This library is licensed under **MIT license**
|
|
66
|
-
|
|
66
|
+
This library is licensed under **MIT license**
|
|
67
|
+
([LICENSE](https://github.com/Electronic-Mango/simple-justwatch-python-api/blob/main/LICENSE)
|
|
68
|
+
or <https://opensource.org/license/MIT>).
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
|
|
@@ -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
|
|
6
|
+
version = "1.1.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
|
|
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
|
|
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,8 @@ class JustWatchApiError(JustWatchError):
|
|
|
43
36
|
|
|
44
37
|
class JustWatchHttpError(JustWatchError):
|
|
45
38
|
"""
|
|
46
|
-
Raised when
|
|
47
|
-
|
|
48
|
-
Any additional verification is not performed, ony the status code is checked.
|
|
49
|
-
|
|
50
|
-
Attributes:
|
|
51
|
-
code (int): HTTP status code returned by the API.
|
|
52
|
-
message (str): HTTP message response from the API.
|
|
39
|
+
Raised when HTTP-related error occurs.
|
|
53
40
|
|
|
41
|
+
This is a general exception for any HTTP-related errors, such as non-`2xx` status
|
|
42
|
+
codes, network errors, timeouts, etc.
|
|
54
43
|
"""
|
|
55
|
-
|
|
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
|
|
@@ -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
|
|
47
|
-
country 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, 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,12 @@ 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:
|
|
535
|
+
exceptions.JustWatchHttpError: HTTP-related error occurred.
|
|
529
536
|
|
|
530
537
|
"""
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
538
|
+
try:
|
|
539
|
+
response = post(_GRAPHQL_API_URL, json=request_json)
|
|
540
|
+
response.raise_for_status()
|
|
541
|
+
except HTTPError as e:
|
|
542
|
+
raise JustWatchHttpError(str(e)) from e
|
|
534
543
|
return response.json()
|
|
@@ -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.
|
|
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
|
-
|
|
18
|
-
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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(
|
|
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/
|
|
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
|
|
File without changes
|
|
File without changes
|