UNPKG

justwatch-api-client

Version:

A client library to interact with the unofficial JustWatch GraphQL API for searching movies/shows and getting streaming information.

1,285 lines (1,272 loc) 61.6 kB
import FetchWithProxyFallback from "super-fetch-proxy"; import timedFunction from "timed-function"; import getProxyList from "getactiveproxies"; // Main class implementation class JustWatchAPI { constructor(timeout) { this.timeout = timeout || 1400; // Default timeout is 1.4 seconds this.fetchWithProxy = new FetchWithProxyFallback(getProxyList, timeout); } async searchByQuery(searchQuery, country = "IN") { const response = await this.fetchWithProxy.fetch(`https://apis.justwatch.com/graphql`, { method: "POST", headers: { accept: "*/*", "accept-language": "en-US,en;q=0.7", "app-version": "3.9.3-web-web", "content-type": "application/json", "device-id": "w74RDcKnCcKDQRzCC1bDiM", priority: "u=1, i", "sec-ch-ua": '"Brave";v="135", "Not-A.Brand";v="8", "Chromium";v="135"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-site", "sec-gpc": "1", Referer: "https://www.justwatch.com/", "Referrer-Policy": "strict-origin-when-cross-origin", }, body: JSON.stringify({ operationName: "GetSuggestedTitles", variables: { country: country, language: "en", first: 4, filter: { searchQuery: searchQuery, includeTitlesWithoutUrl: true, }, }, query: ` query GetSuggestedTitles($country: Country!, $language: Language!, $first: Int!, $filter: TitleFilter) { popularTitles(country: $country, first: $first, filter: $filter) { edges { node { ...SuggestedTitle __typename } __typename } __typename } } fragment SuggestedTitle on MovieOrShow { __typename id objectType objectId content(country: $country, language: $language) { fullPath title originalReleaseYear posterUrl fullPath __typename } watchNowOffer(country: $country, platform: WEB) { id standardWebURL preAffiliatedStandardWebURL package { id packageId __typename } __typename } offers(country: $country, platform: WEB, filter: {preAffiliate: true}) { monetizationType presentationType standardWebURL preAffiliatedStandardWebURL package { id packageId __typename } id __typename } } `, }), }); const data = await response.json(); const searchResults = data?.data?.popularTitles?.edges || []; const searchResult = searchResults.map((result) => { return { title: result?.node?.content?.title || null, originalReleaseYear: result?.node?.content?.originalReleaseYear || null, posterUrl: result?.node?.content?.posterUrl ? `https://images.justwatch.com${result?.node?.content?.posterUrl .replace("{profile}", "s332") .replace("{format}", "webp")}` : "", fullPath: result?.node?.content?.fullPath || null, }; }); return searchResult; } async search(query, country = "IN") { return timedFunction(this.searchByQuery.bind(this), this.timeout, [], query, country); } async getDataByPath(itemFullPath, country = "IN") { const requestBody = { operationName: "GetUrlTitleDetails", variables: { platform: "WEB", excludeTextRecommendationTitle: true, first: 10, fullPath: itemFullPath, language: "en", country: country, episodeMaxLimit: 20, allowSponsoredRecommendations: { pageType: "VIEW_TITLE_DETAIL", placement: "DETAIL_PAGE", language: "en", country: country, applicationContext: { appID: "3.9.3-webapp#e443a12", platform: "webapp", version: "3.9.3", build: "e443a12", isTestBuild: false, }, appId: "3.9.3-webapp#e443a12", platform: "WEB", supportedFormats: ["IMAGE", "VIDEO"], supportedObjectTypes: [ "MOVIE", "SHOW", "GENERIC_TITLE_LIST", "SHOW_SEASON", ], alwaysReturnBidID: true, testingModeForceHoldoutGroup: false, testingMode: false, }, }, query: ` query GetUrlTitleDetails($fullPath: String!, $country: Country!, $language: Language!, $episodeMaxLimit: Int, $platform: Platform! = WEB, $allowSponsoredRecommendations: SponsoredRecommendationsInput, $format: ImageFormat, $backdropProfile: BackdropProfile, $excludeTextRecommendationTitle: Boolean = true, $streamingChartsFilter: StreamingChartsFilter, $first: Int! = 10) { urlV2(fullPath: $fullPath) { id metaDescription metaKeywords metaRobots metaTitle heading1 heading2 htmlContent node { ...TitleDetails ...BuyBoxOffers __typename } __typename } } fragment TitleDetails on MovieOrShowOrSeason { id objectType objectId __typename plexPlayerOffers: offers( country: $country platform: $platform filter: {packages: ["pxp"], preAffiliate: true} ) { ...WatchNowOffer __typename } justwatchTVOffers: offers( country: $country platform: $platform filter: {packages: ["jwt"], preAffiliate: true} ) { ...WatchNowOffer __typename } disneyOffersCount: offerCount( country: $country platform: $platform filter: {packages: ["dnp"]} ) starOffersCount: offerCount( country: $country platform: $platform filter: {packages: ["srp"]} ) uniqueOfferCount: offerCount( country: $country platform: $platform filter: {bestOnly: true} ) offers(country: $country, platform: $platform, filter: {preAffiliate: true}) { ...TitleOffer __typename } watchNowOffer(country: $country, platform: $platform) { ...WatchNowOffer __typename } availableTo(country: $country, platform: $platform) { availableCountDown(country: $country) availableToDate package { id shortName __typename } __typename } fallBackClips: content(country: $country, language: "en") { clips { ...TrailerClips __typename } videobusterClips: clips(providers: [VIDEOBUSTER]) { ...TrailerClips __typename } dailymotionClips: clips(providers: [DAILYMOTION]) { ...TrailerClips __typename } __typename } content(country: $country, language: $language) { backdrops { backdropUrl __typename } fullBackdrops: backdrops(profile: S1920, format: JPG) { backdropUrl __typename } clips { ...TrailerClips __typename } videobusterClips: clips(providers: [VIDEOBUSTER]) { ...TrailerClips __typename } dailymotionClips: clips(providers: [DAILYMOTION]) { ...TrailerClips __typename } externalIds { imdbId wikidataId __typename } fullPath posterUrl fullPosterUrl: posterUrl(profile: S718, format: JPG) runtime isReleased scoring { imdbScore imdbVotes tmdbPopularity tmdbScore jwRating tomatoMeter certifiedFresh __typename } shortDescription title originalReleaseYear originalReleaseDate upcomingReleases { releaseCountDown(country: $country) releaseDate releaseType label package { id packageId shortName clearName monetizationTypes icon(profile: S100) iconWide(profile: S160) hasRectangularIcon(country: $country, platform: WEB) planOffers(country: $country, platform: $platform) { retailPrice(language: $language) durationDays presentationType isTrial retailPriceValue currency __typename } __typename } __typename } genres { shortName translation(language: $language) __typename } subgenres { content(country: $country, language: $language) { shortName name __typename } __typename } textRecommendations(first: $first) { ...TextRecommendation __typename } ... on MovieOrShowOrSeasonContent { subgenres { content(country: $country, language: $language) { url: moviesUrl { fullPath __typename } __typename } __typename } __typename } ... on MovieOrShowContent { originalTitle ageCertification credits { role name characterName personId portraitUrl(profile: S332) __typename } interactions { dislikelistAdditions likelistAdditions votesNumber __typename } productionCountries __typename } ... on MovieContent { tags { technicalName translatedName __typename } __typename } ... on ShowContent { tags { technicalName translatedName __typename } __typename } ... on SeasonContent { seasonNumber interactions { dislikelistAdditions likelistAdditions votesNumber __typename } tags { technicalName translatedName __typename } __typename } __typename } recommendedByCount watchedByCount popularityRank(country: $country) { rank trend trendDifference __typename } streamingCharts(country: $country, filter: $streamingChartsFilter) { edges { streamingChartInfo { rank trend trendDifference updatedAt daysInTop10 daysInTop100 daysInTop1000 daysInTop3 topRank __typename } __typename } __typename } likelistEntry { createdAt __typename } dislikelistEntry { createdAt __typename } ... on MovieOrShow { watchlistEntryV2 { createdAt __typename } customlistEntries { createdAt genericTitleList { id __typename } __typename } similarTitlesV2( country: $country allowSponsoredRecommendations: $allowSponsoredRecommendations ) { sponsoredAd { ...SponsoredAd __typename } __typename } __typename } ... on Movie { permanentAudiences seenlistEntry { createdAt __typename } __typename } ... on Show { permanentAudiences totalSeasonCount seenState(country: $country) { progress seenEpisodeCount __typename } tvShowTrackingEntry { createdAt __typename } offers(country: $country, platform: $platform, filter: {preAffiliate: true}) { ...TitleOffer __typename } seasons(sortDirection: DESC) { id objectId objectType totalEpisodeCount availableTo(country: $country, platform: $platform) { availableToDate availableCountDown(country: $country) package { id shortName __typename } __typename } offers( country: $country platform: $platform filter: {monetizationTypes: [BUY, RENT], preAffiliate: true} ) { package { clearName shortName __typename } monetizationType retailPrice(language: $language) retailPriceValue __typename } content(country: $country, language: $language) { posterUrl seasonNumber fullPath title upcomingReleases { releaseDate releaseCountDown(country: $country) __typename } isReleased originalReleaseYear __typename } show { __typename id objectId objectType watchlistEntryV2 { createdAt __typename } content(country: $country, language: $language) { title __typename } } fallBackClips: content(country: $country, language: "en") { clips { ...TrailerClips __typename } videobusterClips: clips(providers: [VIDEOBUSTER]) { ...TrailerClips __typename } dailymotionClips: clips(providers: [DAILYMOTION]) { ...TrailerClips __typename } __typename } __typename } recentEpisodes: episodes( sortDirection: DESC limit: 3 releasedInCountry: $country ) { ...Episode __typename } __typename } ... on Season { totalEpisodeCount episodes(limit: $episodeMaxLimit) { ...Episode __typename } show { __typename id objectId objectType totalSeasonCount customlistEntries { createdAt genericTitleList { id __typename } __typename } tvShowTrackingEntry { createdAt __typename } fallBackClips: content(country: $country, language: "en") { clips { ...TrailerClips __typename } videobusterClips: clips(providers: [VIDEOBUSTER]) { ...TrailerClips __typename } dailymotionClips: clips(providers: [DAILYMOTION]) { ...TrailerClips __typename } __typename } content(country: $country, language: $language) { title ageCertification fullPath genres { shortName __typename } credits { role name characterName personId __typename } productionCountries externalIds { imdbId __typename } upcomingReleases { releaseDate releaseType package { id shortName planOffers(country: $country, platform: $platform) { retailPrice(language: $language) durationDays presentationType isTrial retailPriceValue currency __typename } __typename } __typename } backdrops { backdropUrl __typename } posterUrl isReleased videobusterClips: clips(providers: [VIDEOBUSTER]) { ...TrailerClips __typename } dailymotionClips: clips(providers: [DAILYMOTION]) { ...TrailerClips __typename } __typename } seenState(country: $country) { progress __typename } watchlistEntryV2 { createdAt __typename } dislikelistEntry { createdAt __typename } likelistEntry { createdAt __typename } similarTitlesV2( country: $country allowSponsoredRecommendations: $allowSponsoredRecommendations ) { sponsoredAd { ...SponsoredAd __typename } __typename } } seenState(country: $country) { progress __typename } __typename } } fragment WatchNowOffer on Offer { __typename id standardWebURL preAffiliatedStandardWebURL streamUrl package { id icon packageId clearName shortName technicalName iconWide(profile: S160) hasRectangularIcon(country: $country, platform: WEB) __typename } retailPrice(language: $language) retailPriceValue lastChangeRetailPriceValue currency presentationType monetizationType availableTo dateCreated newElementCount } fragment TitleOffer on Offer { id presentationType monetizationType newElementCount retailPrice(language: $language) retailPriceValue currency lastChangeRetailPriceValue type package { id packageId clearName shortName technicalName icon(profile: S100) iconWide(profile: S160) planOffers(country: $country, platform: WEB) { title retailPrice(language: $language) isTrial durationDays retailPriceValue children { title retailPrice(language: $language) isTrial durationDays retailPriceValue __typename } __typename } __typename } standardWebURL preAffiliatedStandardWebURL streamUrl elementCount availableTo subtitleLanguages videoTechnology audioTechnology audioLanguages(language: $language) __typename } fragment TrailerClips on Clip { sourceUrl externalId provider name __typename } fragment TextRecommendation on TextRecommendation { __typename id headline body originalHeadline originalBody customProfileType tags { technicalName translatedName __typename } watchedAt watchedOn { ...Package __typename } likeCount likedByUser ownedByUser profile { ...ProfileInfo __typename } updatedAt title @skip(if: $excludeTextRecommendationTitle) { ...PosterTitle __typename } } fragment Package on Package { clearName id shortName technicalName packageId selected monetizationTypes icon addonParent(country: $country, platform: WEB) { id __typename } __typename } fragment ProfileInfo on Profile { __typename id: uuid displayName firstName lastName location country bio avatarUrl isComplete externalUrls { type name url __typename } ownedByUser profileUrl profileType } fragment PosterTitle on MovieOrShowOrSeason { __typename id objectId objectType content(country: $country, language: $language) { title posterUrl fullPath upcomingReleases { releaseDate releaseCountDown(country: $country) __typename } scoring { imdbScore imdbVotes tmdbPopularity tmdbScore jwRating tomatoMeter certifiedFresh __typename } __typename } watchNowOffer(country: $country, platform: $platform) { ...WatchNowOffer __typename } availableTo(country: $country, platform: $platform) { availableToDate availableCountDown(country: $country) __typename } ... on Season { content(country: $country, language: $language) { seasonNumber __typename } show { __typename id objectId objectType content(country: $country, language: $language) { title fullPath __typename } watchNowOffer(country: $country, platform: $platform) { ...WatchNowOffer __typename } } __typename } ...TitleListData } fragment TitleListData on MovieOrShowOrSeason { __typename id objectId objectType dislikelistEntry { createdAt __typename } likelistEntry { createdAt __typename } ... on MovieOrShow { watchlistEntryV2 { createdAt __typename } customlistEntries { createdAt __typename } __typename } ... on Show { seenState(country: $country) { progress __typename } tvShowTrackingEntry { createdAt __typename } __typename } ... on Season { seenState(country: $country) { progress __typename } show { __typename id objectId objectType watchlistEntryV2 { createdAt __typename } seenState(country: $country) { progress __typename } tvShowTrackingEntry { createdAt __typename } customlistEntries { createdAt __typename } } __typename } } fragment SponsoredAd on SponsoredRecommendationAd { bidId holdoutGroup campaign { name backgroundImages { imageURL size __typename } countdownTimer creativeType disclaimerText externalTrackers { type data __typename } hideDetailPageButton hideImdbScore hideJwScore hideRatings hideContent posterOverride promotionalImageUrl promotionalVideo { url __typename } promotionalTitle promotionalText promotionalProviderLogo promotionalProviderWideLogo watchNowLabel watchNowOffer { ...WatchNowOffer __typename } nodeOverrides { nodeId promotionalImageUrl watchNowOffer { standardWebURL __typename } __typename } node { nodeId: id __typename ... on MovieOrShowOrSeason { content(country: $country, language: $language) { fullPath posterUrl title originalReleaseYear scoring { imdbScore jwRating __typename } genres { shortName translation(language: $language) __typename } externalIds { imdbId __typename } backdrops(format: $format, profile: $backdropProfile) { backdropUrl __typename } isReleased __typename } objectId objectType offers(country: $country, platform: $platform, filter: {preAffiliate: true}) { monetizationType presentationType package { id packageId __typename } id __typename } __typename } ... on MovieOrShow { watchlistEntryV2 { createdAt __typename } __typename } ... on Show { seenState(country: $country) { seenEpisodeCount __typename } __typename } ... on Season { content(country: $country, language: $language) { seasonNumber __typename } show { __typename id objectId objectType content(country: $country, language: $language) { originalTitle __typename } watchlistEntryV2 { createdAt __typename } } __typename } ... on GenericTitleList { followedlistEntry { createdAt name __typename } id type content(country: $country, language: $language) { name visibility __typename } titles(country: $country, first: 40) { totalCount edges { cursor node: nodeV2 { content(country: $country, language: $language) { fullPath posterUrl title originalReleaseYear scoring { imdbVotes imdbScore tomatoMeter certifiedFresh jwRating __typename } isReleased __typename } id objectId objectType __typename } __typename } __typename } __typename } } __typename } __typename } fragment Episode on Episode { id objectId objectType seenlistEntry { createdAt __typename } uniqueOfferCount: offerCount( country: $country platform: $platform filter: {bestOnly: true} ) flatrate: offers( country: $country platform: $platform filter: {monetizationTypes: [FLATRATE_AND_BUY, FLATRATE, ADS, CINEMA, FREE], bestOnly: true, preAffiliate: true} ) { id package { id clearName packageId __typename } __typename } buy: offers( country: $country platform: $platform filter: {monetizationTypes: [BUY], bestOnly: true, preAffiliate: true} ) { id package { id clearName packageId __typename } __typename } rent: offers( country: $country platform: $platform filter: {monetizationTypes: [RENT], bestOnly: true, preAffiliate: true} ) { id package { id clearName packageId __typename } __typename } free: offers( country: $country platform: $platform filter: {monetizationTypes: [ADS, FREE], bestOnly: true, preAffiliate: true} ) { id package { id clearName packageId __typename } __typename } fast: offers( country: $country platform: $platform filter: {monetizationTypes: [FAST], bestOnly: true, preAffiliate: true} ) { id package { id clearName packageId __typename } __typename } content(country: $country, language: $language) { title shortDescription episodeNumber seasonNumber isReleased runtime upcomingReleases { releaseDate label package { id packageId __typename } __typename } __typename } __typename } fragment BuyBoxOffers on MovieOrShowOrSeasonOrEpisode { __typename offerCount(country: $country, platform: $platform) maxOfferUpdatedAt(country: $country, platform: $platform) flatrate: offers( country: $country platform: $platform filter: {monetizationTypes: [FLATRATE, FLATRATE_AND_BUY, CINEMA], bestOnly: true, preAffiliate: true} ) { ...TitleOffer __typename } buy: offers( country: $country platform: $platform filter: {monetizationTypes: [BUY], bestOnly: true, preAffiliate: true} ) { ...TitleOffer offerSeasons minRetailPrice(country: $country, platform: $platform, language: $language) __typename } rent: offers( country: $country platform: $platform filter: {monetizationTypes: [RENT], bestOnly: true, preAffiliate: true} ) { ...TitleOffer offerSeasons minRetailPrice(country: $country, p