Split MusicDetails in three and created a first version of ArtistDetails, ReleaseDetails and ReleaseGroupDetails

pull/3800/merge^2
Anatole Sot 3 months ago
parent 2cd4d4fb56
commit ad86646daf

@ -0,0 +1,287 @@
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ListView from '@app/components/Common/ListView';
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 FetchedDataTitleCard from '@app/components/TitleCard/FetchedDataTitleCard';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import Error from '@app/pages/_error';
import { ExclamationTriangleIcon, PlayIcon } from '@heroicons/react/24/outline';
import { MediaStatus, SecondaryType } from '@server/constants/media';
import type { ArtistResult, ReleaseGroupResult } from '@server/models/Search';
import 'country-flag-icons/3x2/flags.css';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import { defineMessage, defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
overview: 'Overview',
playonplex: 'Play on Plex',
reportissue: 'Report an Issue',
releases: 'Releases',
});
const categoriesMessages = {
Album: defineMessage({ id: 'Albums', defaultMessage: 'Albums' }),
Single: defineMessage({ id: 'Singles', defaultMessage: 'Singles' }),
EP: defineMessage({ id: 'EPs', defaultMessage: 'EPs' }),
Other: defineMessage({ id: 'Other', defaultMessage: 'Other' }),
};
interface ArtistDetailsProp {
artist: ArtistResult;
}
const ArtistDetails = ({ artist }: ArtistDetailsProp) => {
const { hasPermission } = useUser();
const router = useRouter();
const intl = useIntl();
const [showIssueModal, setShowIssueModal] = useState(false);
const categorize = (res: ReleaseGroupResult[]) =>
res.reduce((group: { [key: string]: ReleaseGroupResult[] }, item) => {
if (!group[item.type]) {
group[item.type] = [];
}
group[item.type].push(item);
return group;
}, {});
const data = artist;
const { plexUrl } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
});
const mediaLinks: PlayButtonLink[] = [];
if (
plexUrl &&
hasPermission([Permission.REQUEST, Permission.REQUEST_MUSIC], {
type: 'or',
})
) {
mediaLinks.push({
text: intl.formatMessage(messages.playonplex),
url: plexUrl,
svg: <PlayIcon />,
});
}
const cleanDate = (date: Date | string | undefined) => {
date = date ?? '';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const mainDateDisplay: string =
data.beginDate && data.endDate
? `${cleanDate(data.beginDate)} - ${cleanDate(data.endDate)}`
: cleanDate(data.beginDate);
const releaseGroups: ReleaseGroupResult[] = data.releaseGroups;
const categorizedReleaseGroupsType = categorize(releaseGroups);
const [categories, setCategories] = useState(
categorizedReleaseGroupsType ?? {}
);
const customMerge = (
oldData: { [key: string]: ReleaseGroupResult[] },
newData: { [key: string]: ReleaseGroupResult[] }
) => {
for (const key in newData) {
if (oldData[key]) {
oldData[key].push(...newData[key]);
} else {
oldData[key] = newData[key];
}
}
return oldData;
};
const [currentOffset, setCurrentOffset] = useState(0);
const [isLoading, setLoading] = useState(false);
const getMore = useCallback(() => {
if (isLoading) {
return;
}
setLoading(true);
fetch(
`/api/v1/music/artist/${router.query.mbId}?full=true&offset=${
currentOffset + 25
}`
)
.then((res) => res.json())
.then((res) => {
if (res) {
res = categorize(res.releaseGroups ?? []);
setCategories((prev) => customMerge(prev, res));
setCurrentOffset(currentOffset + 25);
setLoading(false);
}
});
}, [currentOffset, isLoading, router.query.mbId]);
useEffect(() => {
const handleScroll = () => {
const bottom =
document.body.scrollHeight - window.scrollY - window.outerHeight <= 1;
if (bottom) {
getMore();
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [getMore]);
if (!data) {
return <Error statusCode={404} />;
}
const title = data.name;
const tags: string[] = data.tags ?? [];
return (
<div
className="media-page"
style={{
height: 493,
}}
>
<div className="media-page-bg-image">
<CachedImage
alt=""
src={data.fanartPath ?? ''}
layout="fill"
objectFit="cover"
priority
/>
<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={SecondaryType.ARTIST}
/>
<div className="media-header">
<div className="media-poster">
<CachedImage
src={data.posterPath ?? ''}
alt={title + ' poster'}
layout="responsive"
width={600}
height={600}
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={SecondaryType.ARTIST}
/>
</div>
<h1 data-testid="media-title">
{title}{' '}
{mainDateDisplay !== '' && (
<span className="media-year">({mainDateDisplay})</span>
)}
</h1>
<span className="media-attributes">
{tags.map((t, k) => (
<span key={k}>{t}</span>
))}
</span>
</div>
<div className="media-actions">
<PlayButton links={mediaLinks} />
<RequestButton
mediaType="music"
media={data.mediaInfo}
mbId={data.id}
secondaryType={SecondaryType.ARTIST}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onUpdate={() => {}}
/>
{data.mediaInfo?.status === 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>
{Object.entries(categories).map(([type, category]) => (
<>
<div className="slider-header">
<div className="slider-title">
<span>
{intl.formatMessage(
categoriesMessages[type as keyof typeof categoriesMessages]
)}
</span>
</div>
</div>
<ListView
isLoading={false}
jsxItems={category.map((item) => (
<FetchedDataTitleCard
key={`media-slider-item-${item.id}`}
data={item}
/>
))}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onScrollBottom={() => {}}
/>
</>
))}
</div>
);
};
export default ArtistDetails;

@ -0,0 +1,228 @@
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import List from '@app/components/Common/List';
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 { Permission, useUser } from '@app/hooks/useUser';
import Error from '@app/pages/_error';
import { ExclamationTriangleIcon, PlayIcon } from '@heroicons/react/24/outline';
import { MediaStatus, SecondaryType } from '@server/constants/media';
import type { RecordingResult, ReleaseResult } 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',
overview: 'Overview',
recommendations: 'Recommendations',
playonplex: 'Play on Plex',
markavailable: 'Mark as Available',
showmore: 'Show More',
showless: 'Show Less',
digitalrelease: 'Digital Release',
physicalrelease: 'Physical Release',
reportissue: 'Report an Issue',
managemusic: 'Manage Music',
releases: 'Releases',
albums: 'Albums',
singles: 'Singles',
eps: 'EPs',
broadcasts: 'Broadcasts',
others: 'Others',
feats: 'Featured In',
tracks: 'Tracks',
});
interface ReleaseDetailsProp {
release: ReleaseResult;
}
const ReleaseDetails = ({ release }: ReleaseDetailsProp) => {
const { hasPermission } = useUser();
const router = useRouter();
const intl = useIntl();
const [showIssueModal, setShowIssueModal] = useState(false);
const { data: fetched } = useSWR<ReleaseResult>(
`/api/v1/music/release/${router.query.mbId}?full=true`
);
const data = fetched ?? release;
const { plexUrl } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
});
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 cleanDate = (date: Date | string | undefined) => {
date = date ?? '';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const mainDateDisplay: string = cleanDate(data.date);
const tracks: RecordingResult[] = data.tracks ?? [];
const lengthToTime = (length: number) => {
length /= 1000;
const minutes = Math.floor(length / 60);
const seconds = length - minutes * 60;
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds.toFixed(0)}`;
};
if (!data) {
return <Error statusCode={404} />;
}
const title = data.title;
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={SecondaryType.RELEASE}
/>
<div className="media-header">
<div className="media-poster">
<CachedImage
src={data.posterPath ?? ''}
alt=""
layout="responsive"
width={600}
height={600}
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={SecondaryType.RELEASE}
/>
</div>
<h1 data-testid="media-title">
{title}{' '}
{mainDateDisplay !== '' && (
<span className="media-year">({mainDateDisplay})</span>
)}
</h1>
<span className="media-attributes">
{tags.map((t, k) => (
<span key={k}>{t}</span>
))}
</span>
</div>
<div className="media-actions">
<PlayButton links={mediaLinks} />
<RequestButton
mediaType="music"
media={data.mediaInfo}
mbId={data.id}
secondaryType={SecondaryType.RELEASE}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onUpdate={() => {}}
/>
{data.mediaInfo?.status === 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>
<List title={intl.formatMessage(messages.tracks)}>
{tracks.map((track, index) => (
<div key={index}>
<div className="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt className="block text-sm font-bold text-gray-400">
{track.title}
</dt>
<dd className="flex text-sm text-white sm:col-span-2 sm:mt-0">
<span className="flex-grow">
{lengthToTime(track.length)}
</span>
</dd>
<dd>
<span className="flex-grow">
{track.artist.map((artist, index) => (
<span key={index}>{artist.name}</span>
))}
</span>
</dd>
</div>
</div>
))}
</List>
</div>
</div>
);
};
export default ReleaseDetails;

@ -0,0 +1,203 @@
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ListView from '@app/components/Common/ListView';
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 FetchedDataTitleCard from '@app/components/TitleCard/FetchedDataTitleCard';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import Error from '@app/pages/_error';
import { ExclamationTriangleIcon, PlayIcon } from '@heroicons/react/24/outline';
import { MediaStatus, SecondaryType } from '@server/constants/media';
import type { ReleaseGroupResult } from '@server/models/Search';
import 'country-flag-icons/3x2/flags.css';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
overview: 'Overview',
recommendations: 'Recommendations',
playonplex: 'Play on Plex',
markavailable: 'Mark as Available',
showmore: 'Show More',
showless: 'Show Less',
reportissue: 'Report an Issue',
releases: 'Releases',
albums: 'Albums',
singles: 'Singles',
eps: 'EPs',
broadcasts: 'Broadcasts',
others: 'Others',
feats: 'Featured In',
});
interface ReleaseGroupDetailsProp {
releaseGroup: ReleaseGroupResult;
}
const ReleaseGroupDetails = ({ releaseGroup }: ReleaseGroupDetailsProp) => {
const { hasPermission } = useUser();
const intl = useIntl();
const [showIssueModal, setShowIssueModal] = useState(false);
const data = releaseGroup;
const { plexUrl } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
});
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 releases = data.releases;
if (!data) {
return <Error statusCode={404} />;
}
/*
const cleanDate = (date: Date | string | undefined) => {
date = date ?? '';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const formatedDate = cleanDate(data.firstReleaseDate ?? '');
*/
const title = data.title;
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={SecondaryType.RELEASE_GROUP}
/>
<div className="media-header">
<div className="media-poster">
<CachedImage
src={data.posterPath ?? ''}
alt={title + ' album cover'}
layout="responsive"
width={600}
height={600}
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={SecondaryType.RELEASE_GROUP}
/>
</div>
<h1 data-testid="media-title">{title}</h1>
<h2 data-testid="media-subtitle">{data.type}</h2>
<span className="media-attributes">
{tags.map((t, k) => (
<span key={k}>{t}</span>
))}
</span>
</div>
<div className="media-actions">
<PlayButton links={mediaLinks} />
<RequestButton
mediaType="music"
media={data.mediaInfo}
mbId={data.id}
secondaryType={SecondaryType.RELEASE_GROUP}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onUpdate={() => {}}
/>
{data.mediaInfo?.status === 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>
{releases?.length > 0 && (
<>
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.releases)}</span>
</div>
</div>
<ListView
isLoading={false}
jsxItems={releases.map((item) => (
<FetchedDataTitleCard
key={`media-slider-item-${item.id}`}
data={item}
/>
))}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onScrollBottom={() => {}}
/>
</>
)}
</div>
);
};
export default ReleaseGroupDetails;

@ -1,61 +1,23 @@
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ListView from '@app/components/Common/ListView';
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 FetchedDataTitleCard from '@app/components/TitleCard/FetchedDataTitleCard';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import ArtistDetails from '@app/components/MusicDetails/ArtistDetails';
import ReleaseDetails from '@app/components/MusicDetails/ReleaseDetails';
import ReleaseGroupDetails from '@app/components/MusicDetails/ReleaseGroupDetails';
import Error from '@app/pages/_error';
import { ExclamationTriangleIcon, PlayIcon } from '@heroicons/react/24/outline';
import { MediaStatus, SecondaryType } from '@server/constants/media';
import { 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 { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
const messages = defineMessages({
originaltitle: 'Original Title',
overview: 'Overview',
recommendations: 'Recommendations',
playonplex: 'Play on Plex',
markavailable: 'Mark as Available',
showmore: 'Show More',
showless: 'Show Less',
digitalrelease: 'Digital Release',
physicalrelease: 'Physical Release',
reportissue: 'Report an Issue',
managemusic: 'Manage Music',
releases: 'Releases',
albums: 'Albums',
singles: 'Singles',
eps: 'EPs',
broadcasts: 'Broadcasts',
others: 'Others',
feats: 'Featured In',
});
interface MusicDetailsProps {
type: SecondaryType;
artist?: ArtistResult;
releaseGroup?: ReleaseGroupResult;
release?: ReleaseResult;
recording?: RecordingResult;
work?: WorkResult;
}
const MusicDetails = ({
@ -63,431 +25,28 @@ const MusicDetails = ({
artist,
releaseGroup,
release,
recording,
work,
}: MusicDetailsProps) => {
const settings = useSettings();
const { hasPermission } = useUser();
const router = useRouter();
const intl = useIntl();
const [showIssueModal, setShowIssueModal] = useState(false);
const { data: fetched } = useSWR<
| ArtistResult
| ReleaseGroupResult
| ReleaseResult
| RecordingResult
| WorkResult
>(`/api/v1/music/${router.query.type}/${router.query.mbId}?full=true`);
let to_explore = [],
data;
switch (type) {
case SecondaryType.ARTIST:
data = fetched ?? artist;
to_explore = [
SecondaryType.RELEASE_GROUP,
SecondaryType.RELEASE,
SecondaryType.RECORDING,
SecondaryType.WORK,
];
break;
return <ArtistDetails artist={(fetched ?? artist) as ArtistResult} />;
case SecondaryType.RELEASE_GROUP:
data = fetched ?? releaseGroup;
to_explore = [
SecondaryType.RELEASE,
SecondaryType.RECORDING,
SecondaryType.WORK,
];
break;
return (
<ReleaseGroupDetails
releaseGroup={(fetched ?? releaseGroup) as ReleaseGroupResult}
/>
);
case SecondaryType.RELEASE:
data = fetched ?? release;
to_explore = [SecondaryType.RECORDING, SecondaryType.ARTIST];
break;
case SecondaryType.RECORDING:
data = fetched ?? recording;
to_explore = [SecondaryType.ARTIST];
break;
case SecondaryType.WORK:
data = fetched ?? work;
to_explore = [SecondaryType.ARTIST];
break;
}
const { plexUrl } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
});
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 cleanDate = (date: Date | string | undefined) => {
date = date ?? '';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const mainDateDisplay: string =
type === SecondaryType.ARTIST &&
cleanDate((data as ArtistResult).beginDate) &&
(data as ArtistResult).endDate
? `${cleanDate((data as ArtistResult).beginDate)} - ${cleanDate(
(data as ArtistResult).endDate
)}`
: type === SecondaryType.ARTIST && (data as ArtistResult).beginDate
? `${cleanDate((data as ArtistResult).beginDate)}`
: type === SecondaryType.RELEASE && (data as ReleaseResult).date
? `${cleanDate((data as ReleaseResult).date)}`
: type === SecondaryType.RECORDING &&
(data as RecordingResult).firstReleased
? `${cleanDate((data as RecordingResult).firstReleased)}`
: '';
const releaseGroups: ReleaseGroupResult[] = to_explore.includes(
SecondaryType.RELEASE_GROUP
)
? (data as ArtistResult).releaseGroups
: type === SecondaryType.RELEASE_GROUP
? [data as ReleaseGroupResult]
: [];
const categorizedReleaseGroupsType = releaseGroups.reduce(
(group: { [key: string]: ReleaseGroupResult[] }, item) => {
if (!group[item.type]) {
group[item.type] = [];
}
group[item.type].push(item);
return group;
},
{}
);
const [albums, setAlbums] = useState(
categorizedReleaseGroupsType['Album'] ?? []
);
const [singles, setSingles] = useState(
categorizedReleaseGroupsType['Single'] ?? []
);
const [eps, setEps] = useState(categorizedReleaseGroupsType['EP'] ?? []);
const [broadcasts, setBroadcasts] = useState(
categorizedReleaseGroupsType['Broadcast'] ?? []
);
const [others, setOthers] = useState(
categorizedReleaseGroupsType['Other'] ?? []
);
const [currentOffset, setCurrentOffset] = useState(0);
const [isLoading, setLoading] = useState(false);
const getMore = useCallback(() => {
if (data?.mediaType === SecondaryType.ARTIST) {
if (isLoading) {
return;
}
setLoading(true);
fetch(
`/api/v1/music/${router.query.type}/${
router.query.mbId
}?full=true&offset=${currentOffset + 25}`
)
.then((res) => res.json())
.then((res) => {
if (res) {
res = (res as ArtistResult).releaseGroups;
res = res.reduce(
((
group: { [key: string]: ReleaseGroupResult[] },
item: ReleaseGroupResult
) => {
if (!group[item.type]) {
group[item.type] = [];
}
group[item.type].push(item);
return group;
}) ?? {},
{}
);
setAlbums((prev) => [...prev, ...(res['Album'] ?? [])]);
setSingles((prev) => [...prev, ...(res.Single ?? [])]);
setEps((prev) => [...prev, ...(res.EP ?? [])]);
setBroadcasts((prev) => [...prev, ...(res.Broadcast ?? [])]);
setOthers((prev) => [...prev, ...(res.Other ?? [])]);
setCurrentOffset(currentOffset + 25);
setLoading(false);
}
});
}
}, [
currentOffset,
data?.mediaType,
isLoading,
router.query.mbId,
router.query.type,
]);
useEffect(() => {
const handleScroll = () => {
const bottom =
document.body.scrollHeight - window.scrollY - window.outerHeight <= 1;
if (bottom) {
getMore();
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [getMore]);
if (!data) {
return <Error statusCode={404} />;
return <ReleaseDetails release={(fetched ?? release) as ReleaseResult} />;
default:
return <Error statusCode={404} />;
}
const title =
data.mediaType !== SecondaryType.ARTIST ? data.title : data.name;
const tags: string[] = data.tags ?? [];
return (
<div
className="media-page"
style={{
height: 493,
}}
>
<div className="media-page-bg-image">
{data.mediaType === SecondaryType.ARTIST && (
<CachedImage
alt=""
src={(data as ArtistResult).fanartPath ?? ''}
layout="fill"
objectFit="cover"
priority
/>
)}
<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.mediaType === SecondaryType.ARTIST
? (data.posterPath as string)
: ''
}
alt=""
layout="responsive"
width={600}
height={600}
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}{' '}
{mainDateDisplay !== '' && (
<span className="media-year">({mainDateDisplay})</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}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onUpdate={() => {}}
/>
{(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>
{albums?.length > 0 && (
<>
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.albums)}</span>
</div>
</div>
<ListView
isLoading={false}
jsxItems={albums.map((item) => (
<FetchedDataTitleCard
key={`media-slider-item-${item.id}`}
data={item}
/>
))}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onScrollBottom={() => {}}
/>
</>
)}
{singles?.length > 0 && (
<>
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.singles)}</span>
</div>
</div>
<ListView
isLoading={false}
jsxItems={singles.map((item) => (
<FetchedDataTitleCard
key={`media-slider-item-${item.id}`}
data={item}
/>
))}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onScrollBottom={() => {}}
/>
</>
)}
{eps?.length > 0 && (
<>
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.eps)}</span>
</div>
</div>
<ListView
isLoading={false}
jsxItems={eps.map((item) => (
<FetchedDataTitleCard
key={`media-slider-item-${item.id}`}
data={item}
/>
))}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onScrollBottom={() => {}}
/>
</>
)}
{broadcasts?.length > 0 && (
<>
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.broadcasts)}</span>
</div>
</div>
<ListView
isLoading={false}
jsxItems={broadcasts.map((item) => (
<FetchedDataTitleCard
key={`media-slider-item-${item.id}`}
data={item}
/>
))}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onScrollBottom={() => {}}
/>
</>
)}
{others?.length > 0 && (
<>
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.others)}</span>
</div>
</div>
<ListView
isLoading={false}
jsxItems={others.map((item) => (
<FetchedDataTitleCard
key={`media-slider-item-${item.id}`}
data={item}
/>
))}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onScrollBottom={() => {}}
/>
</>
)}
</div>
);
};
export default MusicDetails;

Loading…
Cancel
Save