From b8740cf22b93c9e3c0617ea81bbe113b35a7c640 Mon Sep 17 00:00:00 2001 From: Anatole Sot <47571181+ano0002@users.noreply.github.com> Date: Sat, 17 Feb 2024 01:49:06 +0100 Subject: [PATCH] Added the start of the ux to navigate thru artists and music stuff --- server/constants/media.ts | 8 + server/entity/Media.ts | 3 + server/lib/scanners/baseScanner.ts | 4 +- server/models/Search.ts | 8 + .../Discover/RecentlyAddedSlider/index.tsx | 5 +- .../IssueModal/CreateIssueModal/index.tsx | 11 +- src/components/IssueModal/index.tsx | 11 +- src/components/MusicDetails/index.tsx | 294 ++++++++++++++++++ src/components/RequestButton/index.tsx | 14 +- src/components/StatusBadge/index.tsx | 100 +++--- src/components/TitleCard/MusicTitleCard.tsx | 17 +- src/components/TitleCard/index.tsx | 2 +- src/pages/music/[type]/[mbId]/index.tsx | 73 +++++ 13 files changed, 486 insertions(+), 64 deletions(-) create mode 100644 src/components/MusicDetails/index.tsx create mode 100644 src/pages/music/[type]/[mbId]/index.tsx diff --git a/server/constants/media.ts b/server/constants/media.ts index 51b471163..6f56a314c 100644 --- a/server/constants/media.ts +++ b/server/constants/media.ts @@ -11,6 +11,14 @@ export enum MediaType { MUSIC = 'music', } +export enum SecondaryType { + ARTIST = 'artist', + RELEASE_GROUP = 'release-group', + RELEASE = 'release', + RECORDING = 'recording', + WORK = 'work', +} + export enum MediaStatus { UNKNOWN = 1, PENDING, diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 577a9563d..0d4fd482f 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -91,6 +91,9 @@ class Media { @Column({ type: 'varchar' }) public mediaType: MediaType; + @Column({ type: 'varchar', nullable: true }) + public secondaryType?: string; + @Column({ nullable: true }) @Index() public tmdbId?: number; diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index bfd282895..ddc118c23 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -1,5 +1,5 @@ import TheMovieDb from '@server/api/themoviedb'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import { MediaStatus, MediaType, SecondaryType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import Season from '@server/entity/Season'; @@ -567,7 +567,7 @@ class BaseScanner { } else { const newMedia = new Media(); newMedia.mbId = mbId; - + newMedia.secondaryType = SecondaryType.ARTIST; newMedia.status = !processing ? MediaStatus.AVAILABLE : processing diff --git a/server/models/Search.ts b/server/models/Search.ts index f5c042e36..ed2c04a89 100644 --- a/server/models/Search.ts +++ b/server/models/Search.ts @@ -82,6 +82,7 @@ export interface CollectionResult { backdropPath?: string; overview: string; originalLanguage: string; + mediaInfo?: Media; } export interface PersonResult { @@ -92,6 +93,7 @@ export interface PersonResult { adult: boolean; mediaType: 'person'; knownFor: (MovieResult | TvResult)[]; + mediaInfo?: Media; } export interface ReleaseGroupResult { @@ -100,6 +102,7 @@ export interface ReleaseGroupResult { type: mbReleaseGroupType; posterPath?: string; title: string; + releases: ReleaseResult[]; artist: ArtistResult[]; tags: string[]; mediaInfo?: Media; @@ -125,6 +128,7 @@ export interface RecordingResult { length: number; firstReleased?: Date; tags: string[]; + mediaInfo?: Media; } export interface WorkResult { @@ -133,6 +137,7 @@ export interface WorkResult { title: string; artist: ArtistResult[]; tags: string[]; + mediaInfo?: Media; } export interface ArtistResult { @@ -250,6 +255,9 @@ export const mapReleaseGroupResult = ( type: releaseGroupResult.type, title: releaseGroupResult.title, artist: releaseGroupResult.artist.map((artist) => mapArtistResult(artist)), + releases: (releaseGroupResult.releases ?? []).map((release) => + mapReleaseResult(release) + ), tags: releaseGroupResult.tags, posterPath: getPosterFromMB(releaseGroupResult), mediaInfo: media ?? undefined, diff --git a/src/components/Discover/RecentlyAddedSlider/index.tsx b/src/components/Discover/RecentlyAddedSlider/index.tsx index 314ae049f..b27422bcd 100644 --- a/src/components/Discover/RecentlyAddedSlider/index.tsx +++ b/src/components/Discover/RecentlyAddedSlider/index.tsx @@ -2,6 +2,7 @@ import Slider from '@app/components/Slider'; import MusicTitleCard from '@app/components/TitleCard/MusicTitleCard'; import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; import { Permission, useUser } from '@app/hooks/useUser'; +import { SecondaryType } from '@server/constants/media'; import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; @@ -31,7 +32,7 @@ const RecentlyAddedSlider = () => { const videoMedias = (media?.results ?? []).filter((item) => ["movie", "tv"].includes(item.mediaType)) const musicMedias = (media?.results ?? []).filter((item) => !["movie", "tv"].includes(item.mediaType)) - + console.log(musicMedias) return ( <>
@@ -69,7 +70,7 @@ const RecentlyAddedSlider = () => { key={`media-slider-item-${item.id}`} id={item.id} mbId={item.mbId ?? ''} - mediaType={item.mediaType as 'music'} + //type={item.secondaryType as SecondaryType} /> ) )} diff --git a/src/components/IssueModal/CreateIssueModal/index.tsx b/src/components/IssueModal/CreateIssueModal/index.tsx index 397322ca7..7bc0578d7 100644 --- a/src/components/IssueModal/CreateIssueModal/index.tsx +++ b/src/components/IssueModal/CreateIssueModal/index.tsx @@ -6,6 +6,7 @@ import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import { RadioGroup } from '@headlessui/react'; import { ArrowRightCircleIcon } from '@heroicons/react/24/solid'; +import type { SecondaryType } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media'; import type Issue from '@server/entity/Issue'; import type { MovieDetails } from '@server/models/Movie'; @@ -47,8 +48,10 @@ const classNames = (...classes: string[]) => { }; interface CreateIssueModalProps { - mediaType: 'movie' | 'tv'; + mediaType: 'movie' | 'tv' | 'music'; tmdbId?: number; + mbId?: string; + secondaryType?: SecondaryType; onCancel?: () => void; } @@ -56,16 +59,18 @@ const CreateIssueModal = ({ onCancel, mediaType, tmdbId, + mbId, + secondaryType, }: CreateIssueModalProps) => { const intl = useIntl(); const settings = useSettings(); const { hasPermission } = useUser(); const { addToast } = useToasts(); const { data, error } = useSWR( - tmdbId ? `/api/v1/${mediaType}/${tmdbId}` : null + tmdbId ? `/api/v1/${mediaType}/${tmdbId}` : mbId ? `/api/v1/music/${secondaryType}/${mbId}` : null ); - if (!tmdbId) { + if (!tmdbId && (!mbId || !secondaryType)) { return null; } diff --git a/src/components/IssueModal/index.tsx b/src/components/IssueModal/index.tsx index bf7e923ac..6e111f636 100644 --- a/src/components/IssueModal/index.tsx +++ b/src/components/IssueModal/index.tsx @@ -1,15 +1,18 @@ import CreateIssueModal from '@app/components/IssueModal/CreateIssueModal'; import { Transition } from '@headlessui/react'; +import type { SecondaryType } from '@server/constants/media'; interface IssueModalProps { show?: boolean; onCancel: () => void; - mediaType: 'movie' | 'tv'; - tmdbId: number; + mediaType: 'movie' | 'tv' | 'music'; + tmdbId?: number; + mbId?: string; + secondaryType?: SecondaryType; issueId?: never; } -const IssueModal = ({ show, mediaType, onCancel, tmdbId }: IssueModalProps) => ( +const IssueModal = ({ show, mediaType, onCancel, tmdbId, mbId, secondaryType }: IssueModalProps) => ( ( mediaType={mediaType} onCancel={onCancel} tmdbId={tmdbId} + mbId={mbId} + secondaryType={secondaryType} /> ); diff --git a/src/components/MusicDetails/index.tsx b/src/components/MusicDetails/index.tsx new file mode 100644 index 000000000..fc05c47ab --- /dev/null +++ b/src/components/MusicDetails/index.tsx @@ -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 ; + } + + const mediaLinks: PlayButtonLink[] = []; + + if ( + plexUrl && + hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], { + type: 'or', + }) + ) { + mediaLinks.push({ + text: intl.formatMessage(messages.playonplex), + url: plexUrl, + svg: , + }); + } + + 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 ( +
+
+
+
+ + setShowIssueModal(false)} + show={showIssueModal} + mediaType="music" + mbId={data.id} + secondaryType={type as SecondaryType} + /> +
+
+ +
+
+
+ 0} + tmdbId={data.mediaInfo?.tmdbId} + mediaType="music" + plexUrl={plexUrl} + serviceUrl={data.mediaInfo?.serviceUrl} + secondaryType={type} + /> +
+

+ {title}{' '} + {datesDisplay !== '' && ( + ({datesDisplay}) + )} +

+ + {tags.length > 0 && + tags + .map((t, k) => {t}) + .reduce((prev, curr) => ( + <> + {prev} + | + {curr} + + ))} + +
+
+ + 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', + } + ) && ( + + + + )} +
+
+
+ ); +}; + +export default MusicDetails; diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index 56e91810b..80da641ce 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -9,7 +9,11 @@ import { InformationCircleIcon, XMarkIcon, } from '@heroicons/react/24/solid'; -import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; +import { + MediaRequestStatus, + MediaStatus, + SecondaryType, +} from '@server/constants/media'; import type Media from '@server/entity/Media'; import type { MediaRequest } from '@server/entity/MediaRequest'; import axios from 'axios'; @@ -43,10 +47,12 @@ interface ButtonOption { } interface RequestButtonProps { - mediaType: 'movie' | 'tv'; + mediaType: 'movie' | 'tv' | 'music'; onUpdate: () => void; - tmdbId: number; + tmdbId?: number; media?: Media; + mbId?: string; + secondaryType?: SecondaryType; isShowComplete?: boolean; is4kShowComplete?: boolean; } @@ -56,6 +62,8 @@ const RequestButton = ({ onUpdate, media, mediaType, + mbId, + secondaryType, isShowComplete = false, is4kShowComplete = false, }: RequestButtonProps) => { diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx index b60b7af04..cfae37c07 100644 --- a/src/components/StatusBadge/index.tsx +++ b/src/components/StatusBadge/index.tsx @@ -5,6 +5,7 @@ import DownloadBlock from '@app/components/DownloadBlock'; import useSettings from '@app/hooks/useSettings'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; +import type { SecondaryType } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media'; import type { DownloadingItem } from '@server/lib/downloadtracker'; import { defineMessages, useIntl } from 'react-intl'; @@ -26,7 +27,9 @@ interface StatusBadgeProps { plexUrl?: string; serviceUrl?: string; tmdbId?: number; - mediaType?: 'movie' | 'tv'; + secondaryType?: SecondaryType; + mbId?: string; + mediaType?: 'movie' | 'tv' | 'music'; title?: string | string[]; } @@ -38,6 +41,8 @@ const StatusBadge = ({ plexUrl, serviceUrl, tmdbId, + mbId, + secondaryType, mediaType, title, }: StatusBadgeProps) => { @@ -52,50 +57,63 @@ const StatusBadge = ({ return Math.round(((media?.size - media?.sizeLeft) / media?.size) * 100); }; - if ( - mediaType && - plexUrl && - hasPermission( - is4k - ? [ - Permission.REQUEST_4K, - mediaType === 'movie' - ? Permission.REQUEST_4K_MOVIE - : Permission.REQUEST_4K_TV, - ] - : [ - Permission.REQUEST, + if (mediaType && plexUrl) { + if (mediaType === 'music') { + mediaLink = plexUrl; + mediaLinkDescription = intl.formatMessage(messages.playonplex); + } else if ( + hasPermission( + is4k + ? [ + Permission.REQUEST_4K, + mediaType === 'movie' + ? Permission.REQUEST_4K_MOVIE + : Permission.REQUEST_4K_TV, + ] + : [ + Permission.REQUEST, + mediaType === 'movie' + ? Permission.REQUEST_MOVIE + : Permission.REQUEST_TV, + ], + { + type: 'or', + } + ) && + (!is4k || + (mediaType === 'movie' + ? settings.currentSettings.movie4kEnabled + : settings.currentSettings.series4kEnabled)) + ) { + mediaLink = plexUrl; + mediaLinkDescription = intl.formatMessage(messages.playonplex); + } + if (hasPermission(Permission.MANAGE_REQUESTS)) { + if ((mediaType === 'movie' || mediaType === 'tv') && tmdbId) { + mediaLink = `/${mediaType}/${tmdbId}?manage=1`; + mediaLinkDescription = intl.formatMessage(messages.managemedia, { + mediaType: intl.formatMessage( + mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow + ), + }); + } else if (mediaType === 'music' && mbId && secondaryType) { + mediaLink = `/music/${secondaryType}/${mbId}?manage=1`; + mediaLinkDescription = intl.formatMessage(messages.managemedia, { + mediaType: intl.formatMessage(globalMessages.music), + }); + } else if (hasPermission(Permission.ADMIN) && serviceUrl) { + mediaLink = serviceUrl; + mediaLinkDescription = intl.formatMessage(messages.openinarr, { + arr: mediaType === 'movie' - ? Permission.REQUEST_MOVIE - : Permission.REQUEST_TV, - ], - { - type: 'or', + ? 'Radarr' + : mediaType === 'tv' + ? 'Sonarr' + : 'Lidarr', + }); } - ) && - (!is4k || - (mediaType === 'movie' - ? settings.currentSettings.movie4kEnabled - : settings.currentSettings.series4kEnabled)) - ) { - mediaLink = plexUrl; - mediaLinkDescription = intl.formatMessage(messages.playonplex); - } else if (hasPermission(Permission.MANAGE_REQUESTS)) { - if (mediaType && tmdbId) { - mediaLink = `/${mediaType}/${tmdbId}?manage=1`; - mediaLinkDescription = intl.formatMessage(messages.managemedia, { - mediaType: intl.formatMessage( - mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow - ), - }); - } else if (hasPermission(Permission.ADMIN) && serviceUrl) { - mediaLink = serviceUrl; - mediaLinkDescription = intl.formatMessage(messages.openinarr, { - arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', - }); } } - const tooltipContent = (