From 98ece67655a5dffe894974e337a3603afeed0236 Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Sun, 19 Sep 2021 05:11:46 -0400 Subject: [PATCH] feat: list streaming providers on movie/TV detail pages (#1778) * feat: list streaming providers on movie/TV detail pages * fix(ui): add margin to media fact value --- overseerr-api.yml | 35 ++++++++++++++++++++ server/api/themoviedb/index.ts | 5 +-- server/api/themoviedb/interfaces.ts | 21 ++++++++++++ server/models/Movie.ts | 12 ++++--- server/models/Tv.ts | 30 +++++++++-------- server/models/common.ts | 47 +++++++++++++++++++++++++-- src/components/MovieDetails/index.tsx | 19 +++++++++++ src/components/TvDetails/index.tsx | 19 +++++++++++ src/i18n/locale/en.json | 2 ++ src/styles/globals.css | 2 +- 10 files changed, 169 insertions(+), 23 deletions(-) diff --git a/overseerr-api.yml b/overseerr-api.yml index 36248eac..0a1ef5be 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -768,6 +768,10 @@ components: $ref: '#/components/schemas/ExternalIds' mediaInfo: $ref: '#/components/schemas/MediaInfo' + watchProviders: + type: array + items: + $ref: '#/components/schemas/WatchProviders' Episode: type: object properties: @@ -942,6 +946,10 @@ components: $ref: '#/components/schemas/Keyword' mediaInfo: $ref: '#/components/schemas/MediaInfo' + watchProviders: + type: array + items: + $ref: '#/components/schemas/WatchProviders' MediaRequest: type: object properties: @@ -1631,6 +1639,33 @@ components: type: number webpush: type: number + WatchProviders: + type: array + items: + type: object + properties: + iso_3166_1: + type: string + link: + type: string + buy: + type: array + items: + $ref: '#/components/schemas/WatchProviderDetails' + flatrate: + items: + $ref: '#/components/schemas/WatchProviderDetails' + WatchProviderDetails: + type: object + properties: + displayPriority: + type: number + logoPath: + type: string + id: + type: number + name: + type: string securitySchemes: cookieAuth: type: apiKey diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 6e3e13ec..ddc18059 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -170,7 +170,8 @@ class TheMovieDb extends ExternalAPI { { params: { language, - append_to_response: 'credits,external_ids,videos,release_dates', + append_to_response: + 'credits,external_ids,videos,release_dates,watch/providers', }, }, 43200 @@ -196,7 +197,7 @@ class TheMovieDb extends ExternalAPI { params: { language, append_to_response: - 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings', + 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers', }, }, 43200 diff --git a/server/api/themoviedb/interfaces.ts b/server/api/themoviedb/interfaces.ts index c3b02b6c..bd3c2d8b 100644 --- a/server/api/themoviedb/interfaces.ts +++ b/server/api/themoviedb/interfaces.ts @@ -166,6 +166,10 @@ export interface TmdbMovieDetails { }; external_ids: TmdbExternalIds; videos: TmdbVideoResult; + 'watch/providers'?: { + id: number; + results?: { [iso_3166_1: string]: TmdbWatchProviders }; + }; } export interface TmdbVideo { @@ -269,6 +273,10 @@ export interface TmdbTvDetails { results: TmdbKeyword[]; }; videos: TmdbVideoResult; + 'watch/providers'?: { + id: number; + results?: { [iso_3166_1: string]: TmdbWatchProviders }; + }; } export interface TmdbVideoResult { @@ -401,3 +409,16 @@ export interface TmdbNetwork { logo_path?: string; origin_country?: string; } + +export interface TmdbWatchProviders { + link?: string; + buy?: TmdbWatchProviderDetails[]; + flatrate?: TmdbWatchProviderDetails[]; +} + +export interface TmdbWatchProviderDetails { + display_priority?: number; + logo_path?: string; + provider_id: number; + provider_name: string; +} diff --git a/server/models/Movie.ts b/server/models/Movie.ts index 58b4fff6..ff8a96a4 100644 --- a/server/models/Movie.ts +++ b/server/models/Movie.ts @@ -3,18 +3,20 @@ import type { TmdbMovieReleaseResult, TmdbProductionCompany, } from '../api/themoviedb/interfaces'; +import Media from '../entity/Media'; import { - ProductionCompany, - Genre, Cast, Crew, + ExternalIds, + Genre, mapCast, mapCrew, - ExternalIds, mapExternalIds, mapVideos, + mapWatchProviders, + ProductionCompany, + WatchProviders, } from './common'; -import Media from '../entity/Media'; export interface Video { url?: string; @@ -78,6 +80,7 @@ export interface MovieDetails { mediaInfo?: Media; externalIds: ExternalIds; plexUrl?: string; + watchProviders?: WatchProviders[]; } export const mapProductionCompany = ( @@ -136,4 +139,5 @@ export const mapMovieDetails = ( : undefined, externalIds: mapExternalIds(movie.external_ids), mediaInfo: media, + watchProviders: mapWatchProviders(movie['watch/providers']?.results ?? {}), }); diff --git a/server/models/Tv.ts b/server/models/Tv.ts index 0216aada..9f6b2568 100644 --- a/server/models/Tv.ts +++ b/server/models/Tv.ts @@ -1,25 +1,27 @@ +import type { + TmdbNetwork, + TmdbSeasonWithEpisodes, + TmdbTvDetails, + TmdbTvEpisodeResult, + TmdbTvRatingResult, + TmdbTvSeasonResult, +} from '../api/themoviedb/interfaces'; +import type Media from '../entity/Media'; import { - Genre, - ProductionCompany, Cast, Crew, + ExternalIds, + Genre, + Keyword, mapAggregateCast, mapCrew, - ExternalIds, mapExternalIds, - Keyword, mapVideos, + mapWatchProviders, + ProductionCompany, TvNetwork, + WatchProviders, } from './common'; -import type { - TmdbTvEpisodeResult, - TmdbTvSeasonResult, - TmdbTvDetails, - TmdbSeasonWithEpisodes, - TmdbTvRatingResult, - TmdbNetwork, -} from '../api/themoviedb/interfaces'; -import type Media from '../entity/Media'; import { Video } from './Movie'; interface Episode { @@ -102,6 +104,7 @@ export interface TvDetails { externalIds: ExternalIds; keywords: Keyword[]; mediaInfo?: Media; + watchProviders?: WatchProviders[]; } const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({ @@ -213,4 +216,5 @@ export const mapTvDetails = ( name: keyword.name, })), mediaInfo: media, + watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}), }); diff --git a/server/models/common.ts b/server/models/common.ts index be276562..49e2305c 100644 --- a/server/models/common.ts +++ b/server/models/common.ts @@ -1,12 +1,13 @@ import type { - TmdbCreditCast, TmdbAggregateCreditCast, + TmdbCreditCast, TmdbCreditCrew, TmdbExternalIds, TmdbVideo, TmdbVideoResult, + TmdbWatchProviderDetails, + TmdbWatchProviders, } from '../api/themoviedb/interfaces'; - import { Video } from '../models/Movie'; export interface ProductionCompany { @@ -70,6 +71,20 @@ export interface ExternalIds { twitterId?: string; } +export interface WatchProviders { + iso_3166_1: string; + link?: string; + buy?: WatchProviderDetails[]; + flatrate?: WatchProviderDetails[]; +} + +export interface WatchProviderDetails { + displayPriority?: number; + logoPath?: string; + id: number; + name: string; +} + export const mapCast = (person: TmdbCreditCast): Cast => ({ castId: person.cast_id, character: person.character, @@ -124,7 +139,33 @@ export const mapVideos = (videoResult: TmdbVideoResult): Video[] => url: siteUrlCreator(site, key), })); +export const mapWatchProviders = (watchProvidersResult: { + [iso_3166_1: string]: TmdbWatchProviders; +}): WatchProviders[] => + Object.entries(watchProvidersResult).map( + ([iso_3166_1, provider]) => + ({ + iso_3166_1, + link: provider.link, + buy: mapWatchProviderDetails(provider.buy ?? []), + flatrate: mapWatchProviderDetails(provider.flatrate ?? []), + } as WatchProviders) + ); + +export const mapWatchProviderDetails = ( + watchProviderDetails: TmdbWatchProviderDetails[] +): WatchProviderDetails[] => + watchProviderDetails.map( + (provider) => + ({ + displayPriority: provider.display_priority, + logoPath: provider.logo_path, + id: provider.provider_id, + name: provider.provider_name, + } as WatchProviderDetails) + ); + const siteUrlCreator = (site: Video['site'], key: string): string => ({ - YouTube: `https://www.youtube.com/watch?v=${key}/`, + YouTube: `https://www.youtube.com/watch?v=${key}`, }[site]); diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index e8655a21..c4c27ca4 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -77,6 +77,7 @@ const messages = defineMessages({ mark4kavailable: 'Mark as Available in 4K', showmore: 'Show More', showless: 'Show Less', + streamingproviders: 'Currently Streaming On', }); interface MovieDetailsProps { @@ -220,6 +221,10 @@ const MovieDetails: React.FC = ({ movie }) => { ); } + const streamingProviders = + data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) + ?.flatrate ?? []; + return (
= ({ movie }) => {
)} + {!!streamingProviders.length && ( +
+ {intl.formatMessage(messages.streamingproviders)} + + {streamingProviders.map((p) => { + return ( + + {p.name} + + ); + })} + +
+ )}
= ({ tv }) => { ) ?? [] ).length; + const streamingProviders = + data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) + ?.flatrate ?? []; + return (
= ({ tv }) => {
)} + {!!streamingProviders.length && ( +
+ {intl.formatMessage(messages.streamingproviders)} + + {streamingProviders.map((p) => { + return ( + + {p.name} + + ); + })} + +
+ )}