Skip to content

Commit b0ea104

Browse files
Add source code
0 parents  commit b0ea104

3 files changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from httpx import post
2+
3+
from simplejustwatchpythonapi.parser import parse_search_response, MediaEntry
4+
from simplejustwatchpythonapi.requests import prepare_search_request
5+
6+
_GRAPHQL_API_URL = "https://apis.justwatch.com/graphql"
7+
8+
9+
def search(
10+
title: str, country: str = "US", language: str = "en", count: int = 4, best_only: bool = True
11+
) -> list[MediaEntry]:
12+
"""Search JustWatch for given title.
13+
Returns a list of entries up to count.
14+
15+
Args:
16+
title: title to search
17+
country: country to search for offers, "US" by default
18+
language: language of responses, "en" by default
19+
count: how many responses should be returned
20+
best_only: return only best offers if True, return all offers if False
21+
22+
Returns:
23+
List of MediaEntry NamedTuples parsed from JustWatch response
24+
"""
25+
request = prepare_search_request(title, country, language, count, best_only)
26+
response = post(_GRAPHQL_API_URL, json=request)
27+
response.raise_for_status()
28+
return parse_search_response(response.json())
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from typing import NamedTuple
2+
3+
_IMAGES_URL = "https://images.justwatch.com"
4+
5+
6+
class Offer(NamedTuple):
7+
monetization_type: str
8+
presentation_type: str
9+
url: str
10+
price_string: str | None
11+
price_value: float | None
12+
price_currency: str
13+
name: str
14+
technical_name: str
15+
icon: str
16+
17+
18+
class MediaEntry(NamedTuple):
19+
entry_id: str
20+
object_id: int
21+
object_type: str
22+
title: str
23+
url: str
24+
release_year: int
25+
release_date: str
26+
genres: list[str]
27+
imdb_id: list[str]
28+
poster: str
29+
backdrops: list[str]
30+
offers: list[Offer]
31+
32+
33+
def parse_search_response(json: dict) -> list[MediaEntry]:
34+
"""Parse response from search query from JustWatch GraphQL API.
35+
36+
Args:
37+
json: JSON returned by JustWatch GraphQL API
38+
39+
Returns:
40+
Parsed received JSON as a MediaEntry NamedTuple
41+
"""
42+
nodes = json["data"]["popularTitles"]["edges"]
43+
entries = [_parse_entry(node["node"]) for node in nodes]
44+
return entries
45+
46+
47+
def _parse_entry(json: any) -> MediaEntry:
48+
entry_id = json.get("id")
49+
object_id = json.get("objectId")
50+
object_type = json.get("objectType")
51+
content = json["content"]
52+
title = content.get("title")
53+
url = _IMAGES_URL + content.get("fullPath")
54+
year = content.get("originalReleaseYear")
55+
date = content.get("originalReleaseDate")
56+
genres = [node.get("shortName") for node in content.get("genres", []) if node]
57+
external_ids = content.get("externalIds")
58+
imdb_id = external_ids.get("imdbId") if external_ids else None
59+
poster = content.get("posterUrl")
60+
backdrops = [_IMAGES_URL + bd.get("backdropUrl") for bd in content.get("backdrops", []) if bd]
61+
offers = [_parse_offer(offer) for offer in json.get("offers", []) if offer]
62+
return MediaEntry(
63+
entry_id,
64+
object_id,
65+
object_type,
66+
title,
67+
url,
68+
year,
69+
date,
70+
genres,
71+
imdb_id,
72+
poster,
73+
backdrops,
74+
offers,
75+
)
76+
77+
78+
def _parse_offer(json: any) -> Offer:
79+
monetization_type = json.get("monetizationType")
80+
presentation_type = json.get("presentationType")
81+
url = json.get("standardWebURL")
82+
price_string = json.get("retailPrice")
83+
price_value = json.get("retailPriceValue")
84+
price_currency = json.get("currency")
85+
package = json["package"]
86+
name = package.get("clearName")
87+
technical_name = package.get("technicalName")
88+
icon = _IMAGES_URL + package.get("icon")
89+
return Offer(
90+
monetization_type,
91+
presentation_type,
92+
url,
93+
price_string,
94+
price_value,
95+
price_currency,
96+
name,
97+
technical_name,
98+
icon,
99+
)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
GRAPHQL_SEARCH_QUERY = """
2+
query GetSearchTitles(
3+
$searchTitlesFilter: TitleFilter!,
4+
$country: Country!,
5+
$language: Language!,
6+
$first: Int!,
7+
$format: ImageFormat,
8+
$profile: PosterProfile,
9+
$backdropProfile: BackdropProfile,
10+
$filter: OfferFilter!,
11+
) {
12+
popularTitles(
13+
country: $country
14+
filter: $searchTitlesFilter
15+
first: $first
16+
sortBy: POPULAR
17+
sortRandomSeed: 0
18+
) {
19+
edges {
20+
...SearchTitleGraphql
21+
__typename
22+
}
23+
__typename
24+
}
25+
}
26+
27+
fragment SearchTitleGraphql on PopularTitlesEdge {
28+
node {
29+
id
30+
objectId
31+
objectType
32+
content(country: $country, language: $language) {
33+
title
34+
fullPath
35+
originalReleaseYear
36+
originalReleaseDate
37+
genres {
38+
shortName
39+
__typename
40+
}
41+
externalIds {
42+
imdbId
43+
__typename
44+
}
45+
posterUrl(profile: $profile, format: $format)
46+
backdrops(profile: $backdropProfile, format: $format) {
47+
backdropUrl
48+
__typename
49+
}
50+
__typename
51+
}
52+
offers(country: $country, platform: WEB, filter: $filter) {
53+
monetizationType
54+
presentationType
55+
standardWebURL
56+
retailPrice(language: $language)
57+
retailPriceValue
58+
currency
59+
package {
60+
id
61+
packageId
62+
clearName
63+
technicalName
64+
icon(profile: S100)
65+
__typename
66+
}
67+
id
68+
__typename
69+
}
70+
__typename
71+
}
72+
__typename
73+
}
74+
"""
75+
76+
77+
def prepare_search_request(title: str, county: str, lang: str, count: int, best_only: bool) -> dict:
78+
"""Prepare search request for JustWatch GraphQL API.
79+
80+
Args:
81+
title: title to search
82+
county: country to search for offers
83+
lang: language of responses
84+
count: how many responses should be returned
85+
best_only: return only best offers if True, return all offers if False
86+
87+
Returns:
88+
JSON/dict with GraphQL POST body
89+
"""
90+
return {
91+
"operationName": "GetSearchTitles",
92+
"variables": {
93+
"first": count,
94+
"searchTitlesFilter": {"searchQuery": title},
95+
"language": lang,
96+
"country": county,
97+
"format": "JPG",
98+
"profile": "S718",
99+
"backdropProfile": "S1920",
100+
"filter": {"bestOnly": best_only},
101+
},
102+
"query": GRAPHQL_SEARCH_QUERY,
103+
}

0 commit comments

Comments
 (0)