mirror of
https://github.com/NohamR/JustJustWatch.git
synced 2026-07-11 22:40:51 +00:00
236 lines
7.9 KiB
Python
236 lines
7.9 KiB
Python
"""JustWatch availability checker for Plex/Free offers across countries."""
|
|
|
|
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
|
|
import requests
|
|
from tqdm import tqdm
|
|
from tabulate import tabulate
|
|
from InquirerPy import inquirer
|
|
from InquirerPy.base.control import Choice
|
|
|
|
|
|
def country_code_to_flag(code):
|
|
"""Convert ISO 3166-1 alpha-2 country code to regional indicator emoji."""
|
|
return chr(127462 + ord(code[0]) - ord("A")) + chr(127462 + ord(code[1]) - ord("A"))
|
|
|
|
|
|
def format_countries(country_codes):
|
|
"""Format a list of country codes into a human-readable string with flags."""
|
|
representative_flags = ["FR", "GB", "US", "DE"]
|
|
selected_countries = [
|
|
code for code in country_codes if code in representative_flags
|
|
]
|
|
|
|
if len(selected_countries) < len(representative_flags):
|
|
selected_countries += [
|
|
code for code in country_codes if code not in selected_countries
|
|
][: 4 - len(selected_countries)]
|
|
|
|
formatted = ", ".join(
|
|
[f"{country_code_to_flag(code)} {code}" for code in selected_countries]
|
|
)
|
|
if len(country_codes) > len(selected_countries):
|
|
formatted += f" + {len(country_codes) - len(selected_countries)} more"
|
|
return formatted
|
|
|
|
|
|
def update_url_query(url):
|
|
"""Remove UTM tracking parameters from a URL query string."""
|
|
parsed_url = urlparse(url)
|
|
query_params = parse_qs(parsed_url.query)
|
|
keys_to_remove = [
|
|
"utm_source",
|
|
"utm_medium",
|
|
"utm_campaign",
|
|
"utm_term",
|
|
"utm_content",
|
|
]
|
|
for key in keys_to_remove:
|
|
query_params.pop(key, None)
|
|
new_query_string = urlencode(query_params, doseq=True)
|
|
return urlunparse(
|
|
(
|
|
parsed_url.scheme,
|
|
parsed_url.netloc,
|
|
parsed_url.path,
|
|
parsed_url.params,
|
|
new_query_string,
|
|
parsed_url.fragment,
|
|
)
|
|
)
|
|
|
|
|
|
USER_AGENT = (
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
)
|
|
TITLE_DETAILS_QUERY = (
|
|
"query GetUrlTitleDetails($fullPath: String!, $country: Country!, "
|
|
"$language: Language!, $episodeMaxLimit: Int, $platform: Platform! = WEB, "
|
|
"$allowSponsoredRecommendations: SponsoredRecommendationsInput, "
|
|
"$format: ImageFormat, $backdropProfile: BackdropProfile, "
|
|
"$streamingChartsFilter: StreamingChartsFilter) {\n"
|
|
" urlV2(fullPath: $fullPath) {\n node {\n ...TitleDetails\n"
|
|
" __typename\n }\n __typename\n }\n}\n\n"
|
|
"fragment TitleDetails on Node {\n"
|
|
" ... on MovieOrShowOrSeason {\n"
|
|
' plexPlayerOffers: offers(country: $country, platform: $platform, '
|
|
'filter: {packages: ["pxp"]}) {\n'
|
|
" id\n standardWebURL\n"
|
|
" package { id packageId clearName technicalName shortName __typename }\n"
|
|
" __typename\n }\n }\n}"
|
|
)
|
|
|
|
|
|
def search(query):
|
|
"""Search for a title on JustWatch and let the user select one."""
|
|
headers = {"user-agent": USER_AGENT}
|
|
json_data = {
|
|
"operationName": "GetSuggestedTitles",
|
|
"variables": {
|
|
"country": "US",
|
|
"language": "en",
|
|
"first": 5,
|
|
"filter": {
|
|
"searchQuery": query,
|
|
"includeTitlesWithoutUrl": True,
|
|
},
|
|
},
|
|
"query": (
|
|
"query GetSuggestedTitles($country: Country!, $language: Language!, "
|
|
"$first: Int!, $filter: TitleFilter) {\n"
|
|
" popularTitles(country: $country, first: $first, filter: $filter) {\n"
|
|
" edges {\n node {\n ...SuggestedTitle\n __typename\n"
|
|
" }\n __typename\n }\n __typename\n }\n}\n\n"
|
|
"fragment SuggestedTitle on MovieOrShow {\n"
|
|
" __typename\n id\n objectType\n objectId\n"
|
|
" content(country: $country, language: $language) {\n"
|
|
" fullPath\n title\n originalReleaseYear\n posterUrl\n"
|
|
" __typename\n }\n"
|
|
" watchNowOffer(country: $country, platform: WEB) {\n"
|
|
" id\n standardWebURL\n package {\n id\n packageId\n"
|
|
" __typename\n }\n __typename\n }\n"
|
|
" offers(country: $country, platform: WEB) {\n"
|
|
" monetizationType\n presentationType\n standardWebURL\n"
|
|
" package {\n id\n packageId\n __typename\n }\n"
|
|
" id\n __typename\n }\n}\n"
|
|
),
|
|
}
|
|
|
|
response = requests.post(
|
|
"https://apis.justwatch.com/graphql",
|
|
headers=headers, json=json_data, timeout=30
|
|
).json()
|
|
choices = []
|
|
|
|
for edge in response["data"]["popularTitles"]["edges"]:
|
|
node = edge["node"]
|
|
content = node["content"]
|
|
slug = f"{content['title']} ({node['objectType']}) - {content['originalReleaseYear']}"
|
|
|
|
if content["fullPath"] == "":
|
|
slug += " - Unavailable 🚧"
|
|
|
|
choices.append(Choice(value=node, name=slug))
|
|
|
|
if not choices:
|
|
print("No results found.")
|
|
return
|
|
|
|
selected_node = inquirer.fuzzy(
|
|
message="Select a title:",
|
|
choices=choices,
|
|
match_exact=True,
|
|
).execute()
|
|
|
|
path = selected_node["content"]["fullPath"]
|
|
if path == "":
|
|
print("This title is unavailable")
|
|
else:
|
|
print(
|
|
f"Selected: {selected_node['content']['title']} - https://www.justwatch.com{path}"
|
|
)
|
|
fetchdata(path)
|
|
|
|
|
|
def fetchdata(path):
|
|
"""Fetch Plex/Free offer availability for a title across all countries."""
|
|
headers = {"user-agent": USER_AGENT}
|
|
countries = [
|
|
country["iso_3166_2"]
|
|
for country in requests.get(
|
|
"https://apis.justwatch.com/content/locales/state",
|
|
headers=headers, timeout=30
|
|
).json()
|
|
]
|
|
|
|
print(f"Checking {len(countries)} countries...")
|
|
services = {}
|
|
ids = []
|
|
|
|
for country in tqdm(countries, desc="Fetching Availability"):
|
|
json_data = {
|
|
"operationName": "GetUrlTitleDetails",
|
|
"variables": {
|
|
"platform": "WEB",
|
|
"fullPath": path,
|
|
"language": "fr",
|
|
"country": country,
|
|
"episodeMaxLimit": 100,
|
|
},
|
|
"query": TITLE_DETAILS_QUERY,
|
|
}
|
|
|
|
try:
|
|
res = requests.post(
|
|
"https://apis.justwatch.com/graphql",
|
|
headers=headers, json=json_data, timeout=30
|
|
).json()
|
|
data = res.get("data")
|
|
offers = (
|
|
data["urlV2"]["node"].get("plexPlayerOffers", []) if data else []
|
|
)
|
|
|
|
for offer in offers:
|
|
service = offer["package"]["clearName"]
|
|
url = update_url_query(offer["standardWebURL"])
|
|
pkg_id = offer["package"]["id"]
|
|
|
|
if pkg_id not in ids:
|
|
ids.append(pkg_id)
|
|
services[service] = {
|
|
"id": pkg_id,
|
|
"service": service,
|
|
"url": url,
|
|
"countries": [country],
|
|
}
|
|
else:
|
|
if country not in services[service]["countries"]:
|
|
services[service]["countries"].append(country)
|
|
except (KeyError, TypeError, requests.RequestException):
|
|
continue
|
|
|
|
if not services:
|
|
print("\nNo Plex/Free offers found globally.")
|
|
return
|
|
|
|
table_data = [
|
|
[details["service"], format_countries(details["countries"]), details["url"]]
|
|
for details in services.values()
|
|
]
|
|
print(
|
|
"\n"
|
|
+ tabulate(table_data, headers=["Service", "Countries", "URL"], tablefmt="grid")
|
|
)
|
|
|
|
|
|
def main():
|
|
"""Prompt the user for a query and search JustWatch."""
|
|
user_query = inquirer.text(message="Enter the movie/show to search:").execute()
|
|
if user_query:
|
|
search(user_query)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|