mirror of
https://github.com/NohamR/JustJustWatch.git
synced 2025-05-23 16:49:26 +00:00
130 lines
22 KiB
Python
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) |