parent
0c2aa49e80
commit
b8740cf22b
@ -0,0 +1,294 @@
|
|||||||
|
import Button from '@app/components/Common/Button';
|
||||||
|
import CachedImage from '@app/components/Common/CachedImage';
|
||||||
|
import PageTitle from '@app/components/Common/PageTitle';
|
||||||
|
import type { PlayButtonLink } from '@app/components/Common/PlayButton';
|
||||||
|
import PlayButton from '@app/components/Common/PlayButton';
|
||||||
|
import Tooltip from '@app/components/Common/Tooltip';
|
||||||
|
import IssueModal from '@app/components/IssueModal';
|
||||||
|
import RequestButton from '@app/components/RequestButton';
|
||||||
|
import StatusBadge from '@app/components/StatusBadge';
|
||||||
|
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
|
import Error from '@app/pages/_error';
|
||||||
|
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||||
|
import { ExclamationTriangleIcon, PlayIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { MediaStatus, SecondaryType } from '@server/constants/media';
|
||||||
|
import type {
|
||||||
|
ArtistResult,
|
||||||
|
RecordingResult,
|
||||||
|
ReleaseGroupResult,
|
||||||
|
ReleaseResult,
|
||||||
|
WorkResult,
|
||||||
|
} from '@server/models/Search';
|
||||||
|
import 'country-flag-icons/3x2/flags.css';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
originaltitle: 'Original Title',
|
||||||
|
releasedate:
|
||||||
|
'{releaseCount, plural, one {Release Date} other {Release Dates}}',
|
||||||
|
overview: 'Overview',
|
||||||
|
recommendations: 'Recommendations',
|
||||||
|
playonplex: 'Play on Plex',
|
||||||
|
markavailable: 'Mark as Available',
|
||||||
|
showmore: 'Show More',
|
||||||
|
showless: 'Show Less',
|
||||||
|
streamingproviders: 'Currently Streaming On',
|
||||||
|
productioncountries:
|
||||||
|
'Production {countryCount, plural, one {Country} other {Countries}}',
|
||||||
|
digitalrelease: 'Digital Release',
|
||||||
|
physicalrelease: 'Physical Release',
|
||||||
|
reportissue: 'Report an Issue',
|
||||||
|
managemusic: 'Manage Music',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface MusicDetailsProps {
|
||||||
|
type: SecondaryType;
|
||||||
|
artist?: ArtistResult;
|
||||||
|
releaseGroup?: ReleaseGroupResult;
|
||||||
|
release?: ReleaseResult;
|
||||||
|
recording?: RecordingResult;
|
||||||
|
work?: WorkResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MusicDetails = ({
|
||||||
|
type,
|
||||||
|
artist,
|
||||||
|
releaseGroup,
|
||||||
|
release,
|
||||||
|
recording,
|
||||||
|
work,
|
||||||
|
}: MusicDetailsProps) => {
|
||||||
|
const settings = useSettings();
|
||||||
|
const { hasPermission } = useUser();
|
||||||
|
const router = useRouter();
|
||||||
|
const intl = useIntl();
|
||||||
|
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||||
|
|
||||||
|
let data,
|
||||||
|
to_explore = [];
|
||||||
|
switch (type) {
|
||||||
|
case SecondaryType.ARTIST:
|
||||||
|
data = artist;
|
||||||
|
to_explore = [
|
||||||
|
SecondaryType.RELEASE_GROUP,
|
||||||
|
SecondaryType.RELEASE,
|
||||||
|
SecondaryType.RECORDING,
|
||||||
|
SecondaryType.WORK,
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case SecondaryType.RELEASE_GROUP:
|
||||||
|
data = releaseGroup;
|
||||||
|
to_explore = [
|
||||||
|
SecondaryType.RELEASE,
|
||||||
|
SecondaryType.RECORDING,
|
||||||
|
SecondaryType.WORK,
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case SecondaryType.RELEASE:
|
||||||
|
data = release;
|
||||||
|
to_explore = [SecondaryType.RECORDING, SecondaryType.ARTIST];
|
||||||
|
break;
|
||||||
|
case SecondaryType.RECORDING:
|
||||||
|
data = recording;
|
||||||
|
to_explore = [SecondaryType.ARTIST];
|
||||||
|
break;
|
||||||
|
case SecondaryType.WORK:
|
||||||
|
data = work;
|
||||||
|
to_explore = [SecondaryType.ARTIST];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: info,
|
||||||
|
error: errorInfo,
|
||||||
|
mutate: revalidate,
|
||||||
|
} = useSWR<
|
||||||
|
| ArtistResult
|
||||||
|
| ReleaseGroupResult
|
||||||
|
| ReleaseResult
|
||||||
|
| RecordingResult
|
||||||
|
| WorkResult
|
||||||
|
>(`/api/v1/music/${router.query.type}/${router.query.mbId}`, {
|
||||||
|
fallbackData: data as
|
||||||
|
| ArtistResult
|
||||||
|
| ReleaseGroupResult
|
||||||
|
| ReleaseResult
|
||||||
|
| RecordingResult
|
||||||
|
| WorkResult,
|
||||||
|
refreshInterval: refreshIntervalHelper(
|
||||||
|
{
|
||||||
|
downloadStatus: data?.mediaInfo?.downloadStatus,
|
||||||
|
downloadStatus4k: data?.mediaInfo?.downloadStatus4k,
|
||||||
|
},
|
||||||
|
15000
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { plexUrl } = useDeepLinks({
|
||||||
|
plexUrl: data?.mediaInfo?.plexUrl,
|
||||||
|
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <Error statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaLinks: PlayButtonLink[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
plexUrl &&
|
||||||
|
hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], {
|
||||||
|
type: 'or',
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
mediaLinks.push({
|
||||||
|
text: intl.formatMessage(messages.playonplex),
|
||||||
|
url: plexUrl,
|
||||||
|
svg: <PlayIcon />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const title =
|
||||||
|
data.mediaType !== SecondaryType.ARTIST ? data.title : data.name;
|
||||||
|
|
||||||
|
const datesDisplay: string =
|
||||||
|
type === SecondaryType.ARTIST &&
|
||||||
|
(data as ArtistResult).beginDate &&
|
||||||
|
(data as ArtistResult).endDate
|
||||||
|
? `${(data as ArtistResult).beginDate} - ${
|
||||||
|
(data as ArtistResult).endDate
|
||||||
|
}`
|
||||||
|
: type === SecondaryType.ARTIST && (data as ArtistResult).beginDate
|
||||||
|
? `${(data as ArtistResult).beginDate}`
|
||||||
|
: type === SecondaryType.RELEASE && (data as ReleaseResult).date
|
||||||
|
? `${(data as ReleaseResult).date}`
|
||||||
|
: type === SecondaryType.RECORDING &&
|
||||||
|
(data as RecordingResult).firstReleased
|
||||||
|
? `${(data as RecordingResult).firstReleased}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const releases: ReleaseResult[] = to_explore.includes(SecondaryType.RELEASE)
|
||||||
|
? (data as ArtistResult | ReleaseGroupResult).releases
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tags: string[] = data.tags ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="media-page"
|
||||||
|
style={{
|
||||||
|
height: 493,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="media-page-bg-image">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PageTitle title={title} />
|
||||||
|
<IssueModal
|
||||||
|
onCancel={() => setShowIssueModal(false)}
|
||||||
|
show={showIssueModal}
|
||||||
|
mediaType="music"
|
||||||
|
mbId={data.id}
|
||||||
|
secondaryType={type as SecondaryType}
|
||||||
|
/>
|
||||||
|
<div className="media-header">
|
||||||
|
<div className="media-poster">
|
||||||
|
<CachedImage
|
||||||
|
src={
|
||||||
|
data.mediaType === SecondaryType.RELEASE ||
|
||||||
|
data.mediaType === SecondaryType.RELEASE_GROUP
|
||||||
|
? (data.posterPath as string)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
alt=""
|
||||||
|
layout="responsive"
|
||||||
|
width={600}
|
||||||
|
height={900}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="media-title">
|
||||||
|
<div className="media-status">
|
||||||
|
<StatusBadge
|
||||||
|
status={data.mediaInfo?.status}
|
||||||
|
downloadItem={data.mediaInfo?.downloadStatus}
|
||||||
|
title={title}
|
||||||
|
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
|
||||||
|
tmdbId={data.mediaInfo?.tmdbId}
|
||||||
|
mediaType="music"
|
||||||
|
plexUrl={plexUrl}
|
||||||
|
serviceUrl={data.mediaInfo?.serviceUrl}
|
||||||
|
secondaryType={type}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 data-testid="media-title">
|
||||||
|
{title}{' '}
|
||||||
|
{datesDisplay !== '' && (
|
||||||
|
<span className="media-year">({datesDisplay})</span>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
<span className="media-attributes">
|
||||||
|
{tags.length > 0 &&
|
||||||
|
tags
|
||||||
|
.map((t, k) => <span key={k}>{t}</span>)
|
||||||
|
.reduce((prev, curr) => (
|
||||||
|
<>
|
||||||
|
{prev}
|
||||||
|
<span>|</span>
|
||||||
|
{curr}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="media-actions">
|
||||||
|
<PlayButton links={mediaLinks} />
|
||||||
|
<RequestButton
|
||||||
|
mediaType="movie"
|
||||||
|
media={data.mediaInfo}
|
||||||
|
mbId={data.id}
|
||||||
|
secondaryType={type}
|
||||||
|
onUpdate={() => revalidate()}
|
||||||
|
/>
|
||||||
|
{(data.mediaInfo?.status === MediaStatus.AVAILABLE ||
|
||||||
|
(settings.currentSettings.movie4kEnabled &&
|
||||||
|
hasPermission(
|
||||||
|
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
|
||||||
|
{
|
||||||
|
type: 'or',
|
||||||
|
}
|
||||||
|
) &&
|
||||||
|
data.mediaInfo?.status4k === MediaStatus.AVAILABLE)) &&
|
||||||
|
hasPermission(
|
||||||
|
[Permission.CREATE_ISSUES, Permission.MANAGE_ISSUES],
|
||||||
|
{
|
||||||
|
type: 'or',
|
||||||
|
}
|
||||||
|
) && (
|
||||||
|
<Tooltip content={intl.formatMessage(messages.reportissue)}>
|
||||||
|
<Button
|
||||||
|
buttonType="warning"
|
||||||
|
onClick={() => setShowIssueModal(true)}
|
||||||
|
className="ml-2 first:ml-0"
|
||||||
|
>
|
||||||
|
<ExclamationTriangleIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MusicDetails;
|
@ -0,0 +1,73 @@
|
|||||||
|
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||||
|
import MusicDetails from '@app/components/MusicDetails';
|
||||||
|
import Error from '@app/pages/_error';
|
||||||
|
import { SecondaryType } from '@server/constants/media';
|
||||||
|
import type {
|
||||||
|
ArtistResult,
|
||||||
|
RecordingResult,
|
||||||
|
ReleaseGroupResult,
|
||||||
|
ReleaseResult,
|
||||||
|
WorkResult,
|
||||||
|
} from '@server/models/Search';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { GetServerSideProps, NextPage } from 'next';
|
||||||
|
|
||||||
|
interface MusicPageProps {
|
||||||
|
music?:
|
||||||
|
| ArtistResult
|
||||||
|
| ReleaseGroupResult
|
||||||
|
| ReleaseResult
|
||||||
|
| RecordingResult
|
||||||
|
| WorkResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MusicPage: NextPage<MusicPageProps> = ({ music }) => {
|
||||||
|
if (!music) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
switch (music?.mediaType) {
|
||||||
|
case SecondaryType.ARTIST:
|
||||||
|
return <MusicDetails type={SecondaryType.ARTIST} artist={music} />;
|
||||||
|
case SecondaryType.RELEASE_GROUP:
|
||||||
|
return (
|
||||||
|
<MusicDetails type={SecondaryType.RELEASE_GROUP} releaseGroup={music} />
|
||||||
|
);
|
||||||
|
case SecondaryType.RELEASE:
|
||||||
|
return <MusicDetails type={SecondaryType.RELEASE} release={music} />;
|
||||||
|
case SecondaryType.RECORDING:
|
||||||
|
return <MusicDetails type={SecondaryType.RECORDING} recording={music} />;
|
||||||
|
case SecondaryType.WORK:
|
||||||
|
return <MusicDetails type={SecondaryType.WORK} work={music} />;
|
||||||
|
default:
|
||||||
|
return <Error statusCode={404} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<MusicPageProps> = async (
|
||||||
|
ctx
|
||||||
|
) => {
|
||||||
|
const response = await axios.get<
|
||||||
|
| ArtistResult
|
||||||
|
| ReleaseGroupResult
|
||||||
|
| ReleaseResult
|
||||||
|
| RecordingResult
|
||||||
|
| WorkResult
|
||||||
|
>(
|
||||||
|
`http://localhost:${process.env.PORT || 5055}/api/v1/music/${
|
||||||
|
ctx.query.type
|
||||||
|
}/${ctx.query.mbId}`,
|
||||||
|
{
|
||||||
|
headers: ctx.req?.headers?.cookie
|
||||||
|
? { cookie: ctx.req.headers.cookie }
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
music: response.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MusicPage;
|
Loading…
Reference in new issue