diff --git a/overseerr-api.yml b/overseerr-api.yml index 10929b8e4..e99be0475 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -26,6 +26,8 @@ tags: description: Endpoints related to retrieving movies and their details. - name: tv description: Endpoints related to retrieving TV series and their details. + - name: music + description: Endpoints related to retrieving music and details about artists,... - name: other description: Endpoints related to other TMDB data - name: person @@ -666,6 +668,159 @@ components: example: 19923-12-03 mediaInfo: $ref: '#/components/schemas/MediaInfo' + + ArtistResult: + type: object + properties: + id: + type: string + example: 87f17f8a-c0e2-406c-a149-8c8e311bf330 + mediaType: + type: string + example: artist + title: + type: string + example: Album Name + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + name: + type: string + type: + type: string + enum: + - mbArtistType + releases: + type: array + items: + $ref: '#/components/schemas/ReleaseResult' + recordings: + type: array + items: + $ref: '#/components/schemas/RecordingResult' + releaseGroups: + type: array + items: + $ref: '#/components/schemas/ReleaseGroupResult' + works: + type: array + items: + $ref: '#/components/schemas/WorkResult' + gender: + type: string + area: + type: string + beginDate: + type: string + endDate: + type: string + tags: + type: array + items: + type: string + + WorkResult: + type: object + properties: + id: + type: string + example: 87f17f8a-c0e2-406c-a149-8c8e311bf330 + mediaType: + type: string + example: work + title: + type: string + artist: + type: array + items: + $ref: '#/components/schemas/ArtistResult' + tags: + type: array + items: + type: string + + RecordingResult: + type: object + properties: + id: + type: string + example: 87f17f8a-c0e2-406c-a149-8c8e311bf330 + mediaType: + type: string + example: recording + title: + type: string + artist: + type: array + items: + $ref: '#/components/schemas/ArtistResult' + length: + type: number + firstReleased: + type: string + format: date-time + tags: + type: array + items: + type: string + + ReleaseResult: + type: object + properties: + id: + type: string + example: 87f17f8a-c0e2-406c-a149-8c8e311bf330 + mediaType: + type: string + example: release + title: + type: string + artist: + type: array + items: + $ref: '#/components/schemas/ArtistResult' + posterPath: + type: string + date: + type: string + format: date + tracks: + type: array + items: + $ref: '#/components/schemas/RecordingResult' + tags: + type: array + items: + type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' + + ReleaseGroupResult: + type: object + properties: + id: + type: string + example: 87f17f8a-c0e2-406c-a149-8c8e311bf330 + mediaType: + type: string + example: release-group + type: + type: string + enum: + - mbReleaseGroupType + posterPath: + type: string + title: + type: string + artist: + type: array + items: + $ref: '#/components/schemas/ArtistResult' + tags: + type: array + items: + type: string + mediaInfo: + $ref: '#/components/schemas/MediaInfo' Genre: type: object properties: @@ -5932,6 +6087,26 @@ paths: $ref: '#/components/schemas/CreditCrew' id: type: number + /music/artist/{artistId}: + get: + summary: Get artist details + description: Returns full artist details in a JSON object. + tags: + - music + parameters: + - in: path + name: artistId + required: true + schema: + type: string + example: 87f17f8a-c0e2-406c-a149-8c8e311bf330 + responses: + '200': + description: Artist details + content: + application/json: + schema: + $ref: '#/components/schemas/ArtistResult' /media: get: summary: Get media diff --git a/server/entity/Media.ts b/server/entity/Media.ts index ed9e72b00..577a9563d 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -60,17 +60,24 @@ class Media { } public static async getMedia( - id: number, + id: number | string, mediaType: MediaType ): Promise { const mediaRepository = getRepository(Media); try { - const media = await mediaRepository.findOne({ - where: { tmdbId: id, mediaType }, - relations: { requests: true, issues: true }, - }); - + let media: Media | null = null; + if (mediaType === MediaType.MOVIE || mediaType === MediaType.TV) { + media = await mediaRepository.findOne({ + where: { tmdbId: Number(id), mediaType }, + relations: { requests: true, issues: true }, + }); + } else if (mediaType === MediaType.MUSIC) { + media = await mediaRepository.findOne({ + where: { mbId: String(id), mediaType }, + relations: { requests: true, issues: true }, + }); + } return media ?? undefined; } catch (e) { logger.error(e.message); diff --git a/server/models/Search.ts b/server/models/Search.ts index f7d0867da..f5c042e36 100644 --- a/server/models/Search.ts +++ b/server/models/Search.ts @@ -149,6 +149,7 @@ export interface ArtistResult { beginDate?: string; endDate?: string; tags: string[]; + mediaInfo?: Media; } export type Results = @@ -255,7 +256,10 @@ export const mapReleaseGroupResult = ( }; }; -export const mapArtistResult = (artist: mbArtist): ArtistResult => ({ +export const mapArtistResult = ( + artist: mbArtist, + media?: Media +): ArtistResult => ({ id: artist.id, mediaType: 'artist', name: artist.name, @@ -275,6 +279,7 @@ export const mapArtistResult = (artist: mbArtist): ArtistResult => ({ ? artist.works.map((work) => mapWorkResult(work)) : [], tags: artist.tags, + mediaInfo: media ?? undefined, }); export const mapReleaseResult = ( diff --git a/server/routes/index.ts b/server/routes/index.ts index 4ce2507cf..499f8ec71 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -28,6 +28,7 @@ import issueRoutes from './issue'; import issueCommentRoutes from './issueComment'; import mediaRoutes from './media'; import movieRoutes from './movie'; +import musicRoutes from './music'; import personRoutes from './person'; import requestRoutes from './request'; import searchRoutes from './search'; @@ -143,6 +144,7 @@ router.use('/search', isAuthenticated(), searchRoutes); router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/request', isAuthenticated(), requestRoutes); router.use('/movie', isAuthenticated(), movieRoutes); +router.use('/music', isAuthenticated(), musicRoutes); router.use('/tv', isAuthenticated(), tvRoutes); router.use('/media', isAuthenticated(), mediaRoutes); router.use('/person', isAuthenticated(), personRoutes); diff --git a/server/routes/music.ts b/server/routes/music.ts new file mode 100644 index 000000000..ac7960c1f --- /dev/null +++ b/server/routes/music.ts @@ -0,0 +1,32 @@ +import MusicBrainz from '@server/api/musicbrainz'; +import { MediaType } from '@server/constants/media'; +import Media from '@server/entity/Media'; +import logger from '@server/logger'; +import { mapArtistResult } from '@server/models/Search'; +import { Router } from 'express'; + +const musicRoutes = Router(); + +musicRoutes.get('/artist/:id', async (req, res, next) => { + const mb = new MusicBrainz(); + + try { + const artist = await mb.getArtist(req.params.id); + + const media = await Media.getMedia(artist.id, MediaType.MUSIC); + + return res.status(200).json(mapArtistResult(artist, media)); + } catch (e) { + logger.debug('Something went wrong retrieving movie', { + label: 'API', + errorMessage: e.message, + movieId: req.params.id, + }); + return next({ + status: 500, + message: 'Unable to retrieve movie.', + }); + } +}); + +export default musicRoutes; diff --git a/src/components/Discover/RecentlyAddedSlider/index.tsx b/src/components/Discover/RecentlyAddedSlider/index.tsx index 0255be715..314ae049f 100644 --- a/src/components/Discover/RecentlyAddedSlider/index.tsx +++ b/src/components/Discover/RecentlyAddedSlider/index.tsx @@ -1,11 +1,10 @@ import Slider from '@app/components/Slider'; -import TitleCard from '@app/components/TitleCard'; +import MusicTitleCard from '@app/components/TitleCard/MusicTitleCard'; import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; import { Permission, useUser } from '@app/hooks/useUser'; import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; -//import MusicBrainz from '@server/api/musicbrainz'; const messages = defineMessages({ @@ -33,18 +32,6 @@ const RecentlyAddedSlider = () => { const videoMedias = (media?.results ?? []).filter((item) => ["movie", "tv"].includes(item.mediaType)) const musicMedias = (media?.results ?? []).filter((item) => !["movie", "tv"].includes(item.mediaType)) - //const musicBrainz = new MusicBrainz(); - //const artistNames = musicMedias.map(async (item) => {return item.mbId ? (await musicBrainz.getArtist(item.mbId)).name: "Unknown"}); - - const musicItems = musicMedias.map((item) => ( - - )); - return ( <>
@@ -55,15 +42,17 @@ const RecentlyAddedSlider = () => { ( - - ))} + items={ + videoMedias.map((item) => ( + + ) + )} />
@@ -74,7 +63,16 @@ const RecentlyAddedSlider = () => { ( + + ) + )} /> ); diff --git a/src/components/TitleCard/ErrorCard.tsx b/src/components/TitleCard/ErrorCard.tsx index f3c69a65a..55d98565a 100644 --- a/src/components/TitleCard/ErrorCard.tsx +++ b/src/components/TitleCard/ErrorCard.tsx @@ -7,9 +7,10 @@ import { mutate } from 'swr'; interface ErrorCardProps { id: number; - tmdbId: number; + tmdbId?: number; tvdbId?: number; - type: 'movie' | 'tv'; + mbId?: string; + type: 'movie' | 'tv' | 'music'; canExpand?: boolean; } @@ -17,6 +18,7 @@ const messages = defineMessages({ mediaerror: '{mediaType} Not Found', tmdbid: 'TMDB ID', tvdbid: 'TheTVDB ID', + mbId: 'MusicBrainz ID', cleardata: 'Clear Data', }); @@ -44,13 +46,17 @@ const Error = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => {
{type === 'movie' ? intl.formatMessage(globalMessages.movie) - : intl.formatMessage(globalMessages.tvshow)} + : type === 'tv' + ? intl.formatMessage(globalMessages.tvshow) + : intl.formatMessage(globalMessages.music)}
@@ -77,7 +83,9 @@ const Error = ({ id, tmdbId, tvdbId, type, canExpand }: ErrorCardProps) => { mediaType: intl.formatMessage( type === 'movie' ? globalMessages.movie - : globalMessages.tvshow + : type === 'tv' + ? globalMessages.tvshow + : globalMessages.music ), })} diff --git a/src/components/TitleCard/MusicTitleCard.tsx b/src/components/TitleCard/MusicTitleCard.tsx new file mode 100644 index 000000000..d2874a871 --- /dev/null +++ b/src/components/TitleCard/MusicTitleCard.tsx @@ -0,0 +1,81 @@ +import TitleCard from '@app/components/TitleCard'; +import { Permission, useUser } from '@app/hooks/useUser'; +import type { ArtistResult, + ReleaseGroupResult, + ReleaseResult, + WorkResult, + RecordingResult } from '@server/models/Search'; +import { da } from 'date-fns/locale'; +import e from 'express'; +import { has } from 'lodash'; +import { useInView } from 'react-intersection-observer'; +import useSWR from 'swr'; + +export interface MusicBrainTitleCardProps { + id: number; + mbId: string; + mediaType: 'music'; + type?: 'artist' | 'release-group' | 'release' | 'recording' | 'work'; + canExpand?: boolean; +} + +const TmdbTitleCard = ({ + id, + mbId, + mediaType, + canExpand, + type='artist' +}: MusicBrainTitleCardProps) => { + const { hasPermission } = useUser(); + + const { ref, inView } = useInView({ + triggerOnce: true, + }); + const url = `/api/v1/music/${type}/${mbId}`; + const { data, error } = useSWR( + inView ? `${url}` : null + ); + + if (!data && !error) { + return ( +
+ +
+ ); + } + + if (!data) { + return hasPermission(Permission.ADMIN) ? ( + + ) : null; + } + + if (data.mediaType === 'artist') { + const newData = data as ArtistResult; + return ( + + ); + } else if (data.mediaType === 'release-group' || data.mediaType === 'release') { + return () + } + return null; +}; + +export default TmdbTitleCard;