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
This commit is contained in:
@@ -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 {
|
||||
Genre,
|
||||
ProductionCompany,
|
||||
Cast,
|
||||
Crew,
|
||||
mapAggregateCast,
|
||||
mapCrew,
|
||||
ExternalIds,
|
||||
mapExternalIds,
|
||||
Keyword,
|
||||
mapVideos,
|
||||
TvNetwork,
|
||||
} from './common';
|
||||
import type {
|
||||
TmdbTvEpisodeResult,
|
||||
TmdbTvSeasonResult,
|
||||
TmdbTvDetails,
|
||||
TmdbSeasonWithEpisodes,
|
||||
TmdbTvRatingResult,
|
||||
TmdbNetwork,
|
||||
TmdbSeasonWithEpisodes,
|
||||
TmdbTvDetails,
|
||||
TmdbTvEpisodeResult,
|
||||
TmdbTvRatingResult,
|
||||
TmdbTvSeasonResult,
|
||||
} from '../api/themoviedb/interfaces';
|
||||
import type Media from '../entity/Media';
|
||||
import {
|
||||
Cast,
|
||||
Crew,
|
||||
ExternalIds,
|
||||
Genre,
|
||||
Keyword,
|
||||
mapAggregateCast,
|
||||
mapCrew,
|
||||
mapExternalIds,
|
||||
mapVideos,
|
||||
mapWatchProviders,
|
||||
ProductionCompany,
|
||||
TvNetwork,
|
||||
WatchProviders,
|
||||
} from './common';
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user