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'
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

@ -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

@ -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;
}

@ -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 ?? {}),
});

@ -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 ?? {}),
});

@ -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]);

@ -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<MovieDetailsProps> = ({ movie }) => {
);
}
const streamingProviders =
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
?.flatrate ?? [];
return (
<div
className="media-page"
@ -675,6 +680,20 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</span>
</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">
<ExternalLinkBlock
mediaType="movie"

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

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

@ -178,7 +178,7 @@ a.crew-name,
}
.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 {

Loading…
Cancel
Save