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
pull/2099/head^2
TheCatLady 3 years ago committed by GitHub
parent db42c46781
commit 98ece67655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -768,6 +768,10 @@ components:
$ref: '#/components/schemas/ExternalIds' $ref: '#/components/schemas/ExternalIds'
mediaInfo: mediaInfo:
$ref: '#/components/schemas/MediaInfo' $ref: '#/components/schemas/MediaInfo'
watchProviders:
type: array
items:
$ref: '#/components/schemas/WatchProviders'
Episode: Episode:
type: object type: object
properties: properties:
@ -942,6 +946,10 @@ components:
$ref: '#/components/schemas/Keyword' $ref: '#/components/schemas/Keyword'
mediaInfo: mediaInfo:
$ref: '#/components/schemas/MediaInfo' $ref: '#/components/schemas/MediaInfo'
watchProviders:
type: array
items:
$ref: '#/components/schemas/WatchProviders'
MediaRequest: MediaRequest:
type: object type: object
properties: properties:
@ -1631,6 +1639,33 @@ components:
type: number type: number
webpush: webpush:
type: number 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: securitySchemes:
cookieAuth: cookieAuth:
type: apiKey type: apiKey

@ -170,7 +170,8 @@ class TheMovieDb extends ExternalAPI {
{ {
params: { params: {
language, language,
append_to_response: 'credits,external_ids,videos,release_dates', append_to_response:
'credits,external_ids,videos,release_dates,watch/providers',
}, },
}, },
43200 43200
@ -196,7 +197,7 @@ class TheMovieDb extends ExternalAPI {
params: { params: {
language, language,
append_to_response: append_to_response:
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings', 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
}, },
}, },
43200 43200

@ -166,6 +166,10 @@ export interface TmdbMovieDetails {
}; };
external_ids: TmdbExternalIds; external_ids: TmdbExternalIds;
videos: TmdbVideoResult; videos: TmdbVideoResult;
'watch/providers'?: {
id: number;
results?: { [iso_3166_1: string]: TmdbWatchProviders };
};
} }
export interface TmdbVideo { export interface TmdbVideo {
@ -269,6 +273,10 @@ export interface TmdbTvDetails {
results: TmdbKeyword[]; results: TmdbKeyword[];
}; };
videos: TmdbVideoResult; videos: TmdbVideoResult;
'watch/providers'?: {
id: number;
results?: { [iso_3166_1: string]: TmdbWatchProviders };
};
} }
export interface TmdbVideoResult { export interface TmdbVideoResult {
@ -401,3 +409,16 @@ export interface TmdbNetwork {
logo_path?: string; logo_path?: string;
origin_country?: 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;
}

@ -3,18 +3,20 @@ import type {
TmdbMovieReleaseResult, TmdbMovieReleaseResult,
TmdbProductionCompany, TmdbProductionCompany,
} from '../api/themoviedb/interfaces'; } from '../api/themoviedb/interfaces';
import Media from '../entity/Media';
import { import {
ProductionCompany,
Genre,
Cast, Cast,
Crew, Crew,
ExternalIds,
Genre,
mapCast, mapCast,
mapCrew, mapCrew,
ExternalIds,
mapExternalIds, mapExternalIds,
mapVideos, mapVideos,
mapWatchProviders,
ProductionCompany,
WatchProviders,
} from './common'; } from './common';
import Media from '../entity/Media';
export interface Video { export interface Video {
url?: string; url?: string;
@ -78,6 +80,7 @@ export interface MovieDetails {
mediaInfo?: Media; mediaInfo?: Media;
externalIds: ExternalIds; externalIds: ExternalIds;
plexUrl?: string; plexUrl?: string;
watchProviders?: WatchProviders[];
} }
export const mapProductionCompany = ( export const mapProductionCompany = (
@ -136,4 +139,5 @@ export const mapMovieDetails = (
: undefined, : undefined,
externalIds: mapExternalIds(movie.external_ids), externalIds: mapExternalIds(movie.external_ids),
mediaInfo: media, mediaInfo: media,
watchProviders: mapWatchProviders(movie['watch/providers']?.results ?? {}),
}); });

@ -1,25 +1,27 @@
import type {
TmdbNetwork,
TmdbSeasonWithEpisodes,
TmdbTvDetails,
TmdbTvEpisodeResult,
TmdbTvRatingResult,
TmdbTvSeasonResult,
} from '../api/themoviedb/interfaces';
import type Media from '../entity/Media';
import { import {
Genre,
ProductionCompany,
Cast, Cast,
Crew, Crew,
ExternalIds,
Genre,
Keyword,
mapAggregateCast, mapAggregateCast,
mapCrew, mapCrew,
ExternalIds,
mapExternalIds, mapExternalIds,
Keyword,
mapVideos, mapVideos,
mapWatchProviders,
ProductionCompany,
TvNetwork, TvNetwork,
WatchProviders,
} from './common'; } from './common';
import type {
TmdbTvEpisodeResult,
TmdbTvSeasonResult,
TmdbTvDetails,
TmdbSeasonWithEpisodes,
TmdbTvRatingResult,
TmdbNetwork,
} from '../api/themoviedb/interfaces';
import type Media from '../entity/Media';
import { Video } from './Movie'; import { Video } from './Movie';
interface Episode { interface Episode {
@ -102,6 +104,7 @@ export interface TvDetails {
externalIds: ExternalIds; externalIds: ExternalIds;
keywords: Keyword[]; keywords: Keyword[];
mediaInfo?: Media; mediaInfo?: Media;
watchProviders?: WatchProviders[];
} }
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({ const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
@ -213,4 +216,5 @@ export const mapTvDetails = (
name: keyword.name, name: keyword.name,
})), })),
mediaInfo: media, mediaInfo: media,
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
}); });

@ -1,12 +1,13 @@
import type { import type {
TmdbCreditCast,
TmdbAggregateCreditCast, TmdbAggregateCreditCast,
TmdbCreditCast,
TmdbCreditCrew, TmdbCreditCrew,
TmdbExternalIds, TmdbExternalIds,
TmdbVideo, TmdbVideo,
TmdbVideoResult, TmdbVideoResult,
TmdbWatchProviderDetails,
TmdbWatchProviders,
} from '../api/themoviedb/interfaces'; } from '../api/themoviedb/interfaces';
import { Video } from '../models/Movie'; import { Video } from '../models/Movie';
export interface ProductionCompany { export interface ProductionCompany {
@ -70,6 +71,20 @@ export interface ExternalIds {
twitterId?: string; 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 => ({ export const mapCast = (person: TmdbCreditCast): Cast => ({
castId: person.cast_id, castId: person.cast_id,
character: person.character, character: person.character,
@ -124,7 +139,33 @@ export const mapVideos = (videoResult: TmdbVideoResult): Video[] =>
url: siteUrlCreator(site, key), 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 => 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]); }[site]);

@ -77,6 +77,7 @@ const messages = defineMessages({
mark4kavailable: 'Mark as Available in 4K', mark4kavailable: 'Mark as Available in 4K',
showmore: 'Show More', showmore: 'Show More',
showless: 'Show Less', showless: 'Show Less',
streamingproviders: 'Currently Streaming On',
}); });
interface MovieDetailsProps { interface MovieDetailsProps {
@ -220,6 +221,10 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
); );
} }
const streamingProviders =
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
?.flatrate ?? [];
return ( return (
<div <div
className="media-page" className="media-page"
@ -675,6 +680,20 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</span> </span>
</div> </div>
)} )}
{!!streamingProviders.length && (
<div className="media-fact">
<span>{intl.formatMessage(messages.streamingproviders)}</span>
<span className="media-fact-value">
{streamingProviders.map((p) => {
return (
<span className="block" key={`provider-${p.id}`}>
{p.name}
</span>
);
})}
</span>
</div>
)}
<div className="media-fact"> <div className="media-fact">
<ExternalLinkBlock <ExternalLinkBlock
mediaType="movie" mediaType="movie"

@ -80,6 +80,7 @@ const messages = defineMessages({
seasons: '{seasonCount, plural, one {# Season} other {# Seasons}}', seasons: '{seasonCount, plural, one {# Season} other {# Seasons}}',
episodeRuntime: 'Episode Runtime', episodeRuntime: 'Episode Runtime',
episodeRuntimeMinutes: '{runtime} minutes', episodeRuntimeMinutes: '{runtime} minutes',
streamingproviders: 'Currently Streaming On',
}); });
interface TvDetailsProps { interface TvDetailsProps {
@ -235,6 +236,10 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
) ?? [] ) ?? []
).length; ).length;
const streamingProviders =
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
?.flatrate ?? [];
return ( return (
<div <div
className="media-page" className="media-page"
@ -663,6 +668,20 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
</span> </span>
</div> </div>
)} )}
{!!streamingProviders.length && (
<div className="media-fact">
<span>{intl.formatMessage(messages.streamingproviders)}</span>
<span className="media-fact-value">
{streamingProviders.map((p) => {
return (
<span className="block" key={`provider-${p.id}`}>
{p.name}
</span>
);
})}
</span>
</div>
)}
<div className="media-fact"> <div className="media-fact">
<ExternalLinkBlock <ExternalLinkBlock
mediaType="tv" mediaType="tv"

@ -86,6 +86,7 @@
"components.MovieDetails.showless": "Show Less", "components.MovieDetails.showless": "Show Less",
"components.MovieDetails.showmore": "Show More", "components.MovieDetails.showmore": "Show More",
"components.MovieDetails.similar": "Similar Titles", "components.MovieDetails.similar": "Similar Titles",
"components.MovieDetails.streamingproviders": "Currently Streaming On",
"components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studios}}", "components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studios}}",
"components.MovieDetails.viewfullcrew": "View Full Crew", "components.MovieDetails.viewfullcrew": "View Full Crew",
"components.MovieDetails.watchtrailer": "Watch Trailer", "components.MovieDetails.watchtrailer": "Watch Trailer",
@ -700,6 +701,7 @@
"components.TvDetails.seasons": "{seasonCount, plural, one {# Season} other {# Seasons}}", "components.TvDetails.seasons": "{seasonCount, plural, one {# Season} other {# Seasons}}",
"components.TvDetails.showtype": "Series Type", "components.TvDetails.showtype": "Series Type",
"components.TvDetails.similar": "Similar Series", "components.TvDetails.similar": "Similar Series",
"components.TvDetails.streamingproviders": "Currently Streaming On",
"components.TvDetails.viewfullcrew": "View Full Crew", "components.TvDetails.viewfullcrew": "View Full Crew",
"components.TvDetails.watchtrailer": "Watch Trailer", "components.TvDetails.watchtrailer": "Watch Trailer",
"components.UserList.accounttype": "Type", "components.UserList.accounttype": "Type",

@ -178,7 +178,7 @@ a.crew-name,
} }
.media-fact-value { .media-fact-value {
@apply text-sm font-normal text-right text-gray-400; @apply ml-2 text-sm font-normal text-right text-gray-400;
} }
.media-ratings { .media-ratings {

Loading…
Cancel
Save