JustJustWatch/info.py
2024-11-19 00:28:35 +01:00

130 lines
22 KiB
Python

import requests
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
from pprint import pprint
import json
from tqdm import tqdm
from tabulate import tabulate
from simple_term_menu import TerminalMenu
def country_code_to_flag(code):
return chr(127462 + ord(code[0]) - ord('A')) + chr(127462 + ord(code[1]) - ord('A'))
def format_countries(country_codes):
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)]
if len(country_codes) > len(selected_countries):
return ', '.join([f"{country_code_to_flag(code)} {code}" for code in selected_countries]) + f" + {len(country_codes) - len(selected_countries)} more"
else:
return ', '.join([f"{country_code_to_flag(code)} {code}" for code in selected_countries])
def update_url_query(url):
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)
query_params.update(query_params)
new_query_string = urlencode(query_params, doseq=True)
new_url = urlunparse((
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
new_query_string,
parsed_url.fragment
))
return new_url
def search(query):
headers = {'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',}
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\nfragment 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 fullPath\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).json()
options = []
for title in response['data']['popularTitles']['edges']:
title = title['node']
contentTitle = title['content']['title']
contentPath = title['content']['fullPath']
objectType = title['objectType']
originalReleaseYear = title['content']['originalReleaseYear']
slug = f"{contentTitle} ({objectType}) - {originalReleaseYear}"
if contentPath == '':
slug += ' - Unavailable ' + chr(0x1F6A7)
options.append({'title': contentTitle, 'path': contentPath, 'objectType': objectType, 'originalReleaseYear': originalReleaseYear, 'slug': slug})
options_slug = [option['slug'] for option in options]
terminal_menu = TerminalMenu(options_slug)
nameselected = options[terminal_menu.show()]
if nameselected['path'] == '':
print('This title is unavailable')
else:
print(f"Selected: {nameselected['slug']} - {'https://www.justwatch.com' + nameselected['path']}")
path = nameselected['path']
fetchdata(path)
def fetchdata(path):
headers = {'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',}
countries = [country['iso_3166_2'] for country in requests.get('https://apis.justwatch.com/content/locales/state', headers=headers).json()]
print(f"Got {len(countries)} countries")
SERVICES = {}
IDS = []
for country in tqdm(countries):
json_data = {
'operationName': 'GetUrlTitleDetails',
'variables': {
'platform': 'WEB',
'fullPath': path,
'language': 'fr',
'country': country,
'episodeMaxLimit': 100,
},
'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 id\n metaDescription\n metaKeywords\n metaRobots\n metaTitle\n heading1\n heading2\n htmlContent\n node {\n ...TitleDetails\n __typename\n }\n __typename\n }\n}\n\nfragment TitleDetails on Node {\n id\n __typename\n ... on MovieOrShowOrSeason {\n plexPlayerOffers: offers(\n country: $country\n platform: $platform\n filter: {packages: ["pxp"]}\n ) {\n id\n standardWebURL\n package {\n id\n packageId\n clearName\n technicalName\n shortName\n __typename\n }\n __typename\n }\n maxOfferUpdatedAt(country: $country, platform: WEB)\n appleOffers: offers(\n country: $country\n platform: $platform\n filter: {packages: ["atp", "itu"]}\n ) {\n ...TitleOffer\n __typename\n }\n disneyOffersCount: offerCount(\n country: $country\n platform: $platform\n filter: {packages: ["dnp"]}\n )\n starOffersCount: offerCount(\n country: $country\n platform: $platform\n filter: {packages: ["srp"]}\n )\n objectType\n objectId\n offerCount(country: $country, platform: $platform)\n uniqueOfferCount: offerCount(\n country: $country\n platform: $platform\n filter: {bestOnly: true}\n )\n offers(country: $country, platform: $platform) {\n monetizationType\n elementCount\n package {\n id\n packageId\n clearName\n __typename\n }\n __typename\n }\n watchNowOffer(country: $country, platform: $platform) {\n id\n standardWebURL\n __typename\n }\n promotedBundles(country: $country, platform: $platform) {\n promotionUrl\n __typename\n }\n availableTo(country: $country, platform: $platform) {\n availableCountDown(country: $country)\n availableToDate\n package {\n id\n shortName\n __typename\n }\n __typename\n }\n fallBackClips: content(country: $country, language: "en") {\n clips {\n ...TrailerClips\n __typename\n }\n videobusterClips: clips(providers: [VIDEOBUSTER]) {\n ...TrailerClips\n __typename\n }\n dailymotionClips: clips(providers: [DAILYMOTION]) {\n ...TrailerClips\n __typename\n }\n __typename\n }\n content(country: $country, language: $language) {\n backdrops {\n backdropUrl\n __typename\n }\n fullBackdrops: backdrops(profile: S1920, format: JPG) {\n backdropUrl\n __typename\n }\n clips {\n ...TrailerClips\n __typename\n }\n videobusterClips: clips(providers: [VIDEOBUSTER]) {\n ...TrailerClips\n __typename\n }\n dailymotionClips: clips(providers: [DAILYMOTION]) {\n ...TrailerClips\n __typename\n }\n externalIds {\n imdbId\n __typename\n }\n fullPath\n posterUrl\n fullPosterUrl: posterUrl(profile: S718, format: JPG)\n runtime\n isReleased\n scoring {\n imdbScore\n imdbVotes\n tmdbPopularity\n tmdbScore\n jwRating\n tomatoMeter\n certifiedFresh\n __typename\n }\n shortDescription\n title\n originalReleaseYear\n originalReleaseDate\n upcomingReleases(releaseTypes: DIGITAL) {\n releaseCountDown(country: $country)\n releaseDate\n label\n package {\n id\n packageId\n shortName\n clearName\n icon(profile: S100)\n hasRectangularIcon(country: $country, platform: WEB)\n __typename\n }\n __typename\n }\n genres {\n shortName\n translation(language: $language)\n __typename\n }\n subgenres {\n content(country: $country, language: $language) {\n shortName\n name\n __typename\n }\n __typename\n }\n ... on MovieOrShowOrSeasonContent {\n subgenres {\n content(country: $country, language: $language) {\n url: moviesUrl {\n fullPath\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n ... on MovieOrShowContent {\n originalTitle\n ageCertification\n credits {\n role\n name\n characterName\n personId\n __typename\n }\n interactions {\n dislikelistAdditions\n likelistAdditions\n votesNumber\n __typename\n }\n productionCountries\n __typename\n }\n ... on SeasonContent {\n seasonNumber\n interactions {\n dislikelistAdditions\n likelistAdditions\n votesNumber\n __typename\n }\n __typename\n }\n __typename\n }\n popularityRank(country: $country) {\n rank\n trend\n trendDifference\n __typename\n }\n streamingCharts(country: $country, filter: $streamingChartsFilter) {\n edges {\n streamingChartInfo {\n rank\n trend\n trendDifference\n updatedAt\n daysInTop10\n daysInTop100\n daysInTop1000\n daysInTop3\n topRank\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n ... on MovieOrShowOrSeason {\n likelistEntry {\n createdAt\n __typename\n }\n dislikelistEntry {\n createdAt\n __typename\n }\n __typename\n }\n ... on MovieOrShow {\n watchlistEntryV2 {\n createdAt\n __typename\n }\n customlistEntries {\n createdAt\n genericTitleList {\n id\n __typename\n }\n __typename\n }\n similarTitlesV2(\n country: $country\n allowSponsoredRecommendations: $allowSponsoredRecommendations\n ) {\n sponsoredAd {\n ...SponsoredAd\n __typename\n }\n __typename\n }\n __typename\n }\n ... on Movie {\n permanentAudiences\n seenlistEntry {\n createdAt\n __typename\n }\n __typename\n }\n ... on Show {\n permanentAudiences\n totalSeasonCount\n seenState(country: $country) {\n progress\n seenEpisodeCount\n __typename\n }\n tvShowTrackingEntry {\n createdAt\n __typename\n }\n seasons(sortDirection: DESC) {\n id\n objectId\n objectType\n totalEpisodeCount\n availableTo(country: $country, platform: $platform) {\n availableToDate\n availableCountDown(country: $country)\n package {\n id\n shortName\n __typename\n }\n __typename\n }\n content(country: $country, language: $language) {\n posterUrl\n seasonNumber\n fullPath\n title\n upcomingReleases(releaseTypes: DIGITAL) {\n releaseDate\n releaseCountDown(country: $country)\n package {\n id\n shortName\n __typename\n }\n __typename\n }\n isReleased\n originalReleaseYear\n __typename\n }\n show {\n id\n objectId\n objectType\n watchlistEntryV2 {\n createdAt\n __typename\n }\n content(country: $country, language: $language) {\n title\n __typename\n }\n __typename\n }\n fallBackClips: content(country: $country, language: "en") {\n clips {\n ...TrailerClips\n __typename\n }\n videobusterClips: clips(providers: [VIDEOBUSTER]) {\n ...TrailerClips\n __typename\n }\n dailymotionClips: clips(providers: [DAILYMOTION]) {\n ...TrailerClips\n __typename\n }\n __typename\n }\n __typename\n }\n recentEpisodes: episodes(\n sortDirection: DESC\n limit: 3\n releasedInCountry: $country\n ) {\n ...Episode\n __typename\n }\n __typename\n }\n ... on Season {\n totalEpisodeCount\n episodes(limit: $episodeMaxLimit) {\n ...Episode\n __typename\n }\n show {\n id\n objectId\n objectType\n totalSeasonCount\n customlistEntries {\n createdAt\n genericTitleList {\n id\n __typename\n }\n __typename\n }\n tvShowTrackingEntry {\n createdAt\n __typename\n }\n fallBackClips: content(country: $country, language: "en") {\n clips {\n ...TrailerClips\n __typename\n }\n videobusterClips: clips(providers: [VIDEOBUSTER]) {\n ...TrailerClips\n __typename\n }\n dailymotionClips: clips(providers: [DAILYMOTION]) {\n ...TrailerClips\n __typename\n }\n __typename\n }\n content(country: $country, language: $language) {\n title\n ageCertification\n fullPath\n genres {\n shortName\n __typename\n }\n credits {\n role\n name\n characterName\n personId\n __typename\n }\n productionCountries\n externalIds {\n imdbId\n __typename\n }\n upcomingReleases(releaseTypes: DIGITAL) {\n releaseDate\n __typename\n }\n backdrops {\n backdropUrl\n __typename\n }\n posterUrl\n isReleased\n videobusterClips: clips(providers: [VIDEOBUSTER]) {\n ...TrailerClips\n __typename\n }\n dailymotionClips: clips(providers: [DAILYMOTION]) {\n ...TrailerClips\n __typename\n }\n __typename\n }\n seenState(country: $country) {\n progress\n __typename\n }\n watchlistEntryV2 {\n createdAt\n __typename\n }\n dislikelistEntry {\n createdAt\n __typename\n }\n likelistEntry {\n createdAt\n __typename\n }\n similarTitlesV2(\n country: $country\n allowSponsoredRecommendations: $allowSponsoredRecommendations\n ) {\n sponsoredAd {\n ...SponsoredAd\n __typename\n }\n __typename\n }\n __typename\n }\n seenState(country: $country) {\n progress\n __typename\n }\n __typename\n }\n}\n\nfragment TitleOffer on Offer {\n id\n presentationType\n monetizationType\n retailPrice(language: $language)\n retailPriceValue\n currency\n lastChangeRetailPriceValue\n type\n package {\n id\n packageId\n clearName\n technicalName\n icon(profile: S100)\n planOffers(country: $country, platform: WEB) {\n title\n retailPrice(language: $language)\n isTrial\n durationDays\n retailPriceValue\n children {\n title\n retailPrice(language: $language)\n isTrial\n durationDays\n retailPriceValue\n __typename\n }\n __typename\n }\n hasRectangularIcon(country: $country, platform: WEB)\n __typename\n }\n standardWebURL\n elementCount\n availableTo\n deeplinkRoku: deeplinkURL(platform: ROKU_OS)\n subtitleLanguages\n videoTechnology\n audioTechnology\n audioLanguages(language: $language)\n __typename\n}\n\nfragment TrailerClips on Clip {\n sourceUrl\n externalId\n provider\n name\n __typename\n}\n\nfragment SponsoredAd on SponsoredRecommendationAd {\n bidId\n holdoutGroup\n campaign {\n name\n externalTrackers {\n type\n data\n __typename\n }\n hideRatings\n hideDetailPageButton\n promotionalImageUrl\n promotionalVideo {\n url\n __typename\n }\n promotionalTitle\n promotionalText\n promotionalProviderLogo\n watchNowLabel\n watchNowOffer {\n standardWebURL\n presentationType\n monetizationType\n package {\n id\n packageId\n shortName\n clearName\n icon\n __typename\n }\n __typename\n }\n nodeOverrides {\n nodeId\n promotionalImageUrl\n watchNowOffer {\n standardWebURL\n __typename\n }\n __typename\n }\n node {\n nodeId: id\n __typename\n ... on MovieOrShowOrSeason {\n content(country: $country, language: $language) {\n fullPath\n posterUrl\n title\n originalReleaseYear\n scoring {\n imdbScore\n __typename\n }\n externalIds {\n imdbId\n __typename\n }\n backdrops(format: $format, profile: $backdropProfile) {\n backdropUrl\n __typename\n }\n isReleased\n __typename\n }\n objectId\n objectType\n offers(country: $country, platform: $platform) {\n monetizationType\n presentationType\n package {\n id\n packageId\n __typename\n }\n id\n __typename\n }\n __typename\n }\n ... on MovieOrShow {\n watchlistEntryV2 {\n createdAt\n __typename\n }\n __typename\n }\n ... on Show {\n seenState(country: $country) {\n seenEpisodeCount\n __typename\n }\n __typename\n }\n ... on Season {\n content(country: $country, language: $language) {\n seasonNumber\n __typename\n }\n show {\n __typename\n id\n content(country: $country, language: $language) {\n originalTitle\n __typename\n }\n watchlistEntryV2 {\n createdAt\n __typename\n }\n }\n __typename\n }\n ... on GenericTitleList {\n followedlistEntry {\n createdAt\n name\n __typename\n }\n id\n type\n content(country: $country, language: $language) {\n name\n visibility\n __typename\n }\n titles(country: $country, first: 40) {\n totalCount\n edges {\n cursor\n node: nodeV2 {\n content(country: $country, language: $language) {\n fullPath\n posterUrl\n title\n originalReleaseYear\n scoring {\n imdbScore\n __typename\n }\n isReleased\n __typename\n }\n id\n objectId\n objectType\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n }\n __typename\n }\n __typename\n}\n\nfragment Episode on Episode {\n id\n objectId\n objectType\n seenlistEntry {\n createdAt\n __typename\n }\n content(country: $country, language: $language) {\n title\n shortDescription\n episodeNumber\n seasonNumber\n isReleased\n runtime\n upcomingReleases {\n releaseDate\n label\n package {\n id\n packageId\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n',
}
response = requests.post('https://apis.justwatch.com/graphql', headers=headers, json=json_data).json()
# last_updated = response['data']['urlV2']['node']['maxOfferUpdatedAt']
# print('last_updated: ', last_updated)
for offer in response['data']['urlV2']['node']['plexPlayerOffers']:
service = offer['package']['clearName']
url = update_url_query(offer['standardWebURL'])
id = offer['package']['id']
if id not in IDS:
IDS.append(id)
infos = {'id': id,'service': service,'url': url,'countries': [country],}
SERVICES[service] = infos
else:
if country not in SERVICES[service]['countries']:
SERVICES[service]['countries'].append(country)
table_data = []
for service, details in SERVICES.items():
formatted_countries = format_countries(details["countries"])
table_data.append([details["service"], formatted_countries, details["url"]])
headers = ["Service", "Countries", "URL"]
print(tabulate(table_data, headers, tablefmt="grid"))
return SERVICES
# url = 'https://www.justwatch.com/fr/serie/mad-dogs' # /fr/serie/mad-dogs
# url = input('Enter the URL: ')
# path = url.replace('https://www.justwatch.com', '')
# services = fetchdata(path)
# with open('services.json', 'w') as f:
# json.dump(services, f, indent=4)
query = input('Enter the query: ')
search(query)