Merge branch 'develop'

master
sct 2 years ago
commit 7dd683c3d8

@ -138,6 +138,7 @@ location ^~ /overseerr {
sub_filter 'href="/"' 'href="/$app"'; sub_filter 'href="/"' 'href="/$app"';
sub_filter 'href="/login"' 'href="/$app/login"'; sub_filter 'href="/login"' 'href="/$app/login"';
sub_filter 'href:"/"' 'href:"/$app"'; sub_filter 'href:"/"' 'href:"/$app"';
sub_filter '\/_next' '\/$app\/_next';
sub_filter '/_next' '/$app/_next'; sub_filter '/_next' '/$app/_next';
sub_filter '/api/v1' '/$app/api/v1'; sub_filter '/api/v1' '/$app/api/v1';
sub_filter '/login/plex/loading' '/$app/login/plex/loading'; sub_filter '/login/plex/loading' '/$app/login/plex/loading';

@ -4585,9 +4585,13 @@ paths:
type: number type: number
example: 123 example: 123
seasons: seasons:
type: array oneOf:
items: - type: array
type: number items:
type: number
minimum: 1
- type: string
enum: [all]
is4k: is4k:
type: boolean type: boolean
example: false example: false
@ -4666,7 +4670,7 @@ paths:
$ref: '#/components/schemas/MediaRequest' $ref: '#/components/schemas/MediaRequest'
put: put:
summary: Update MediaRequest summary: Update MediaRequest
description: Updates a specific media request and returns the request in a JSON object.. Requires the `MANAGE_REQUESTS` permission. description: Updates a specific media request and returns the request in a JSON object. Requires the `MANAGE_REQUESTS` permission.
tags: tags:
- request - request
parameters: parameters:
@ -4677,6 +4681,37 @@ paths:
example: '1' example: '1'
schema: schema:
type: string type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
mediaType:
type: string
enum: [movie, tv]
seasons:
type: array
items:
type: number
minimum: 1
is4k:
type: boolean
example: false
serverId:
type: number
profileId:
type: number
rootFolder:
type: string
languageProfileId:
type: number
userId:
type: number
nullable: true
required:
- mediaType
responses: responses:
'200': '200':
description: Succesfully updated request description: Succesfully updated request

@ -191,7 +191,7 @@ export interface TmdbVideo {
export interface TmdbTvEpisodeResult { export interface TmdbTvEpisodeResult {
id: number; id: number;
air_date: string; air_date: string | null;
episode_number: number; episode_number: number;
name: string; name: string;
overview: string; overview: string;
@ -372,7 +372,8 @@ export interface TmdbPersonCombinedCredits {
crew: TmdbPersonCreditCrew[]; crew: TmdbPersonCreditCrew[];
} }
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult { export interface TmdbSeasonWithEpisodes
extends Omit<TmdbTvSeasonResult, 'episode_count'> {
episodes: TmdbTvEpisodeResult[]; episodes: TmdbTvEpisodeResult[];
external_ids: TmdbExternalIds; external_ids: TmdbExternalIds;
} }

@ -63,7 +63,7 @@ export const startJobs = (): void => {
id: 'plex-watchlist-sync', id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync', name: 'Plex Watchlist Sync',
type: 'process', type: 'process',
interval: 'long', interval: 'short',
cronSchedule: jobs['plex-watchlist-sync'].schedule, cronSchedule: jobs['plex-watchlist-sync'].schedule,
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => { job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', { logger.info('Starting scheduled job: Plex Watchlist Sync', {

@ -29,7 +29,7 @@ import type { Video } from './Movie';
interface Episode { interface Episode {
id: number; id: number;
name: string; name: string;
airDate: string; airDate: string | null;
episodeNumber: number; episodeNumber: number;
overview: string; overview: string;
productionCode: string; productionCode: string;
@ -50,7 +50,7 @@ interface Season {
seasonNumber: number; seasonNumber: number;
} }
export interface SeasonWithEpisodes extends Season { export interface SeasonWithEpisodes extends Omit<Season, 'episodeCount'> {
episodes: Episode[]; episodes: Episode[];
externalIds: ExternalIds; externalIds: ExternalIds;
} }
@ -141,7 +141,6 @@ export const mapSeasonWithEpisodes = (
season: TmdbSeasonWithEpisodes season: TmdbSeasonWithEpisodes
): SeasonWithEpisodes => ({ ): SeasonWithEpisodes => ({
airDate: season.air_date, airDate: season.air_date,
episodeCount: season.episode_count,
episodes: season.episodes.map(mapEpisodeResult), episodes: season.episodes.map(mapEpisodeResult),
externalIds: mapExternalIds(season.external_ids), externalIds: mapExternalIds(season.external_ids),
id: season.id, id: season.id,

@ -1,5 +1,7 @@
import TheMovieDb from '@server/api/themoviedb'; import TheMovieDb from '@server/api/themoviedb';
import { MediaStatus } from '@server/constants/media';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import { import {
mapCastCredits, mapCastCredits,
@ -34,6 +36,7 @@ personRoutes.get('/:id', async (req, res, next) => {
personRoutes.get('/:id/combined_credits', async (req, res, next) => { personRoutes.get('/:id/combined_credits', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const settings = getSettings();
try { try {
const combinedCredits = await tmdb.getPersonCombinedCredits({ const combinedCredits = await tmdb.getPersonCombinedCredits({
@ -41,14 +44,30 @@ personRoutes.get('/:id/combined_credits', async (req, res, next) => {
language: req.locale ?? (req.query.language as string), language: req.locale ?? (req.query.language as string),
}); });
const castMedia = await Media.getRelatedMedia( let castMedia = await Media.getRelatedMedia(
combinedCredits.cast.map((result) => result.id) combinedCredits.cast.map((result) => result.id)
); );
const crewMedia = await Media.getRelatedMedia( let crewMedia = await Media.getRelatedMedia(
combinedCredits.crew.map((result) => result.id) combinedCredits.crew.map((result) => result.id)
); );
if (settings.main.hideAvailable) {
castMedia = castMedia.filter(
(media) =>
(media.mediaType === 'movie' || media.mediaType === 'tv') &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.PARTIALLY_AVAILABLE
);
crewMedia = crewMedia.filter(
(media) =>
(media.mediaType === 'movie' || media.mediaType === 'tv') &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
return res.status(200).json({ return res.status(200).json({
cast: combinedCredits.cast cast: combinedCredits.cast
.map((result) => .map((result) =>

@ -61,7 +61,7 @@ function Button<P extends ElementTypes = 'button'>(
break; break;
case 'warning': case 'warning':
buttonStyle.push( buttonStyle.push(
'text-white border border-yellow-500 backdrop-blur bg-yellow-500 bg-opacity-80 hover:bg-opacity-100 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-opacity-100 active:border-yellow-700' 'text-white border border-yellow-500 bg-yellow-500 bg-opacity-80 hover:bg-opacity-100 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-opacity-100 active:border-yellow-700'
); );
break; break;
case 'success': case 'success':

@ -20,7 +20,7 @@ const Tooltip = ({ children, content, tooltipConfig }: TooltipProps) => {
return ( return (
<> <>
{React.cloneElement(children, { ref: setTriggerRef })} {React.cloneElement(children, { ref: setTriggerRef })}
{visible && ( {visible && content && (
<div <div
ref={setTooltipRef} ref={setTooltipRef}
{...getTooltipProps({ {...getTooltipProps({

@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
import IssueComment from '@app/components/IssueDetails/IssueComment'; import IssueComment from '@app/components/IssueDetails/IssueComment';
import IssueDescription from '@app/components/IssueDetails/IssueDescription'; import IssueDescription from '@app/components/IssueDetails/IssueDescription';
import { issueOptions } from '@app/components/IssueModal/constants'; import { issueOptions } from '@app/components/IssueModal/constants';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
@ -88,6 +89,13 @@ const IssueDetails = () => {
: null : null
); );
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
plexUrl4k: data?.mediaInfo?.plexUrl4k,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
});
const CommentSchema = Yup.object().shape({ const CommentSchema = Yup.object().shape({
message: Yup.string().required(), message: Yup.string().required(),
}); });
@ -354,7 +362,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl && ( {issueData?.media.plexUrl && (
<Button <Button
as="a" as="a"
href={issueData?.media.plexUrl} href={plexUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="w-full" className="w-full"
@ -387,7 +395,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl4k && ( {issueData?.media.plexUrl4k && (
<Button <Button
as="a" as="a"
href={issueData?.media.plexUrl4k} href={plexUrl4k}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="w-full" className="w-full"
@ -590,7 +598,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl && ( {issueData?.media.plexUrl && (
<Button <Button
as="a" as="a"
href={issueData?.media.plexUrl} href={plexUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="w-full" className="w-full"
@ -623,7 +631,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl4k && ( {issueData?.media.plexUrl4k && (
<Button <Button
as="a" as="a"
href={issueData?.media.plexUrl4k} href={plexUrl4k}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="w-full" className="w-full"

@ -120,7 +120,7 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
> >
<> <>
<div className="sidebar relative flex h-full w-full max-w-xs flex-1 flex-col bg-gray-800"> <div className="sidebar relative flex h-full w-full max-w-xs flex-1 flex-col bg-gray-800">
<div className="sidebar-close-button absolute top-0 right-0 -mr-14 p-1"> <div className="sidebar-close-button absolute right-0 -mr-14 p-1">
<button <button
className="flex h-12 w-12 items-center justify-center rounded-full focus:bg-gray-600 focus:outline-none" className="flex h-12 w-12 items-center justify-center rounded-full focus:bg-gray-600 focus:outline-none"
aria-label="Close sidebar" aria-label="Close sidebar"

@ -18,6 +18,7 @@ import PersonCard from '@app/components/PersonCard';
import RequestButton from '@app/components/RequestButton'; import RequestButton from '@app/components/RequestButton';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import StatusBadge from '@app/components/StatusBadge'; import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale'; import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
@ -123,29 +124,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false); setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]); }, [router.query.manage]);
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.plexUrl); const { plexUrl, plexUrl4k } = useDeepLinks({
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.plexUrl4k); plexUrl: data?.mediaInfo?.plexUrl,
plexUrl4k: data?.mediaInfo?.plexUrl4k,
useEffect(() => { iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
if (data) { iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
if ( });
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)
) {
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
} else {
setPlexUrl(data.mediaInfo?.plexUrl);
setPlexUrl4k(data.mediaInfo?.plexUrl4k);
}
}
}, [
data,
data?.mediaInfo?.iOSPlexUrl,
data?.mediaInfo?.iOSPlexUrl4k,
data?.mediaInfo?.plexUrl,
data?.mediaInfo?.plexUrl4k,
]);
if (!data && !error) { if (!data && !error) {
return <LoadingSpinner />; return <LoadingSpinner />;
@ -324,7 +308,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="movie" mediaType="movie"
plexUrl={data.mediaInfo?.plexUrl} plexUrl={plexUrl}
serviceUrl={data.mediaInfo?.serviceUrl} serviceUrl={data.mediaInfo?.serviceUrl}
/> />
{settings.currentSettings.movie4kEnabled && {settings.currentSettings.movie4kEnabled &&
@ -346,7 +330,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
} }
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="movie" mediaType="movie"
plexUrl={data.mediaInfo?.plexUrl4k} plexUrl={plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k} serviceUrl={data.mediaInfo?.serviceUrl4k}
/> />
)} )}

@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
import Tooltip from '@app/components/Common/Tooltip'; import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal'; import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge'; import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import { withProperties } from '@app/utils/typeHelpers'; import { withProperties } from '@app/utils/typeHelpers';
@ -61,6 +62,13 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
const { hasPermission } = useUser(); const { hasPermission } = useUser();
const intl = useIntl(); const intl = useIntl();
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const deleteRequest = async () => { const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${requestData?.media.id}`); await axios.delete(`/api/v1/media/${requestData?.media.id}`);
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded'); mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
@ -138,11 +146,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
).length > 0 ).length > 0
} }
is4k={requestData.is4k} is4k={requestData.is4k}
plexUrl={ plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={ serviceUrl={
requestData.is4k requestData.is4k
? requestData.media.serviceUrl4k ? requestData.media.serviceUrl4k
@ -217,6 +221,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
fallbackData: request, fallbackData: request,
}); });
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const modifyRequest = async (type: 'approve' | 'decline') => { const modifyRequest = async (type: 'approve' | 'decline') => {
const response = await axios.post(`/api/v1/request/${request.id}/${type}`); const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
@ -396,11 +407,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
is4k={requestData.is4k} is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId} tmdbId={requestData.media.tmdbId}
mediaType={requestData.type} mediaType={requestData.type}
plexUrl={ plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={ serviceUrl={
requestData.is4k requestData.is4k
? requestData.media.serviceUrl4k ? requestData.media.serviceUrl4k

@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton'; import ConfirmButton from '@app/components/Common/ConfirmButton';
import RequestModal from '@app/components/RequestModal'; import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge'; import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import { import {
@ -61,6 +62,13 @@ const RequestItemError = ({
revalidateList(); revalidateList();
}; };
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
return ( return (
<div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row"> <div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row">
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row"> <div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
@ -130,11 +138,7 @@ const RequestItemError = ({
).length > 0 ).length > 0
} }
is4k={requestData.is4k} is4k={requestData.is4k}
plexUrl={ plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={ serviceUrl={
requestData.is4k requestData.is4k
? requestData.media.serviceUrl4k ? requestData.media.serviceUrl4k
@ -316,6 +320,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
} }
}; };
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
if (!title && !error) { if (!title && !error) {
return ( return (
<div <div
@ -462,11 +473,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
is4k={requestData.is4k} is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId} tmdbId={requestData.media.tmdbId}
mediaType={requestData.type} mediaType={requestData.type}
plexUrl={ plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={ serviceUrl={
requestData.is4k requestData.is4k
? requestData.media.serviceUrl4k ? requestData.media.serviceUrl4k

@ -309,7 +309,7 @@ const SettingsMain = () => {
</div> </div>
</div> </div>
<div className="form-row"> <div className="form-row">
<label htmlFor="csrfProtection" className="checkbox-label"> <label htmlFor="cacheImages" className="checkbox-label">
<span className="mr-2"> <span className="mr-2">
{intl.formatMessage(messages.cacheImages)} {intl.formatMessage(messages.cacheImages)}
</span> </span>

@ -77,7 +77,7 @@ const StatusBadge = ({
mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow
), ),
}); });
} else if (hasPermission(Permission.ADMIN)) { } else if (hasPermission(Permission.ADMIN) && serviceUrl) {
mediaLink = serviceUrl; mediaLink = serviceUrl;
mediaLinkDescription = intl.formatMessage(messages.openinarr, { mediaLinkDescription = intl.formatMessage(messages.openinarr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',

@ -6,6 +6,7 @@ import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages({
somethingwentwrong: 'Something went wrong while retrieving season data.', somethingwentwrong: 'Something went wrong while retrieving season data.',
noepisodes: 'Episode list unavailable.',
}); });
type SeasonProps = { type SeasonProps = {
@ -29,32 +30,38 @@ const Season = ({ seasonNumber, tvId }: SeasonProps) => {
return ( return (
<div className="flex flex-col justify-center divide-y divide-gray-700"> <div className="flex flex-col justify-center divide-y divide-gray-700">
{data.episodes {data.episodes.length === 0 ? (
.slice() <p>{intl.formatMessage(messages.noepisodes)}</p>
.reverse() ) : (
.map((episode) => { data.episodes
return ( .slice()
<div .reverse()
className="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4" .map((episode) => {
key={`season-${seasonNumber}-episode-${episode.episodeNumber}`} return (
> <div
<div className="flex-1"> className="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
<div className="flex flex-col space-y-2 xl:flex-row xl:items-center xl:space-y-0 xl:space-x-2"> key={`season-${seasonNumber}-episode-${episode.episodeNumber}`}
<h3 className="text-lg">{episode.name}</h3> >
<AirDateBadge airDate={episode.airDate} /> <div className="flex-1">
<div className="flex flex-col space-y-2 xl:flex-row xl:items-center xl:space-y-0 xl:space-x-2">
<h3 className="text-lg">{episode.name}</h3>
{episode.airDate && (
<AirDateBadge airDate={episode.airDate} />
)}
</div>
{episode.overview && <p>{episode.overview}</p>}
</div> </div>
{episode.overview && <p>{episode.overview}</p>} {episode.stillPath && (
<img
className="h-auto w-full rounded-lg xl:h-32 xl:w-auto"
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
alt=""
/>
)}
</div> </div>
{episode.stillPath && ( );
<img })
className="h-auto w-full rounded-lg xl:h-32 xl:w-auto" )}
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
alt=""
/>
)}
</div>
);
})}
</div> </div>
); );
}; };

@ -22,6 +22,7 @@ import RequestModal from '@app/components/RequestModal';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import StatusBadge from '@app/components/StatusBadge'; import StatusBadge from '@app/components/StatusBadge';
import Season from '@app/components/TvDetails/Season'; import Season from '@app/components/TvDetails/Season';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale'; import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
@ -122,29 +123,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false); setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]); }, [router.query.manage]);
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.plexUrl); const { plexUrl, plexUrl4k } = useDeepLinks({
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.plexUrl4k); plexUrl: data?.mediaInfo?.plexUrl,
plexUrl4k: data?.mediaInfo?.plexUrl4k,
useEffect(() => { iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
if (data) { iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
if ( });
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)
) {
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
} else {
setPlexUrl(data.mediaInfo?.plexUrl);
setPlexUrl4k(data.mediaInfo?.plexUrl4k);
}
}
}, [
data,
data?.mediaInfo?.iOSPlexUrl,
data?.mediaInfo?.iOSPlexUrl4k,
data?.mediaInfo?.plexUrl,
data?.mediaInfo?.plexUrl4k,
]);
if (!data && !error) { if (!data && !error) {
return <LoadingSpinner />; return <LoadingSpinner />;
@ -337,7 +321,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="tv" mediaType="tv"
plexUrl={data.mediaInfo?.plexUrl} plexUrl={plexUrl}
serviceUrl={data.mediaInfo?.serviceUrl} serviceUrl={data.mediaInfo?.serviceUrl}
/> />
{settings.currentSettings.series4kEnabled && {settings.currentSettings.series4kEnabled &&
@ -359,7 +343,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
} }
tmdbId={data.mediaInfo?.tmdbId} tmdbId={data.mediaInfo?.tmdbId}
mediaType="tv" mediaType="tv"
plexUrl={data.mediaInfo?.plexUrl4k} plexUrl={plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k} serviceUrl={data.mediaInfo?.serviceUrl4k}
/> />
)} )}
@ -829,6 +813,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</div> </div>
)} )}
{data.nextEpisodeToAir && {data.nextEpisodeToAir &&
data.nextEpisodeToAir.airDate &&
data.nextEpisodeToAir.airDate !== data.firstAirDate && ( data.nextEpisodeToAir.airDate !== data.firstAirDate && (
<div className="media-fact"> <div className="media-fact">
<span>{intl.formatMessage(messages.nextAirDate)}</span> <span>{intl.formatMessage(messages.nextAirDate)}</span>

@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
interface useDeepLinksProps {
plexUrl?: string;
plexUrl4k?: string;
iOSPlexUrl?: string;
iOSPlexUrl4k?: string;
}
const useDeepLinks = ({
plexUrl,
plexUrl4k,
iOSPlexUrl,
iOSPlexUrl4k,
}: useDeepLinksProps) => {
const [returnedPlexUrl, setReturnedPlexUrl] = useState(plexUrl);
const [returnedPlexUrl4k, setReturnedPlexUrl4k] = useState(plexUrl4k);
useEffect(() => {
if (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)
) {
setReturnedPlexUrl(iOSPlexUrl);
setReturnedPlexUrl4k(iOSPlexUrl4k);
} else {
setReturnedPlexUrl(plexUrl);
setReturnedPlexUrl4k(plexUrl4k);
}
}, [iOSPlexUrl, iOSPlexUrl4k, plexUrl, plexUrl4k]);
return { plexUrl: returnedPlexUrl, plexUrl4k: returnedPlexUrl4k };
};
export default useDeepLinks;

@ -881,6 +881,7 @@
"components.TitleCard.mediaerror": "{mediaType} Not Found", "components.TitleCard.mediaerror": "{mediaType} Not Found",
"components.TitleCard.tmdbid": "TMDB ID", "components.TitleCard.tmdbid": "TMDB ID",
"components.TitleCard.tvdbid": "TheTVDB ID", "components.TitleCard.tvdbid": "TheTVDB ID",
"components.TvDetails.Season.noepisodes": "Episode list unavailable.",
"components.TvDetails.Season.somethingwentwrong": "Something went wrong while retrieving season data.", "components.TvDetails.Season.somethingwentwrong": "Something went wrong while retrieving season data.",
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast", "components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew", "components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",

Loading…
Cancel
Save