diff --git a/server/api/coverartarchive/index.ts b/server/api/coverartarchive/index.ts index 6422d8be..6b63e71b 100644 --- a/server/api/coverartarchive/index.ts +++ b/server/api/coverartarchive/index.ts @@ -150,42 +150,84 @@ class CoverArtArchive extends ExternalAPI { ): Promise> { if (!ids.length) return {}; + const validIds = ids.filter( + (id) => + typeof id === 'string' && + /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test( + id + ) + ); + + if (!validIds.length) return {}; + const metadataRepository = getRepository(MetadataAlbum); const existingMetadata = await metadataRepository.find({ - where: { mbAlbumId: In(ids) }, + where: { mbAlbumId: In(validIds) }, select: ['mbAlbumId', 'caaUrl', 'updatedAt'], }); + const metadataMap = new Map( + existingMetadata.map((metadata) => [metadata.mbAlbumId, metadata]) + ); + const results: Record = {}; const idsToFetch: string[] = []; - ids.forEach((id) => { - const metadata = existingMetadata.find((m) => m.mbAlbumId === id); + for (const id of validIds) { + const metadata = metadataMap.get(id); if (metadata?.caaUrl) { - results[id] = metadata.caaUrl; + Object.defineProperty(results, id, { + value: metadata.caaUrl, + enumerable: true, + writable: true, + }); } else if (metadata && !this.isMetadataStale(metadata)) { - results[id] = null; + Object.defineProperty(results, id, { + value: null, + enumerable: true, + writable: true, + }); } else { idsToFetch.push(id); } - }); + } if (idsToFetch.length > 0) { - const batchPromises = idsToFetch.map((id) => - this.fetchCoverArt(id) - .then((response) => { - const frontImage = response.images.find((img) => img.front); - results[id] = frontImage?.thumbnails?.[250] || null; - return true; - }) - .catch(() => { - results[id] = null; - return false; - }) - ); + try { + const batchPromises = idsToFetch.map((id) => + this.fetchCoverArt(id) + .then((response) => { + const frontImage = response.images.find((img) => img.front); + Object.defineProperty(results, id, { + value: frontImage?.thumbnails?.[250] || null, + enumerable: true, + writable: true, + }); + return true; + }) + .catch((error) => { + logger.error('Failed to fetch cover art', { + label: 'CoverArtArchive', + id, + error: error instanceof Error ? error.message : 'Unknown error', + }); + Object.defineProperty(results, id, { + value: null, + enumerable: true, + writable: true, + }); + return false; + }) + ); - await Promise.all(batchPromises); + await Promise.all(batchPromises); + } catch (error) { + logger.error('Failed to process cover art requests', { + label: 'CoverArtArchive', + error: error instanceof Error ? error.message : 'Unknown error', + }); + } } return results; diff --git a/server/api/musicbrainz/index.ts b/server/api/musicbrainz/index.ts index 6271383c..f57b868e 100644 --- a/server/api/musicbrainz/index.ts +++ b/server/api/musicbrainz/index.ts @@ -98,16 +98,27 @@ class MusicBrainz extends ExternalAPI { artistMbid: string; language?: string; }): Promise<{ title: string; url: string; content: string } | null> { + if ( + !artistMbid || + typeof artistMbid !== 'string' || + !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test( + artistMbid + ) + ) { + throw new Error('Invalid MusicBrainz artist ID format'); + } + try { - const response = await fetch( - `https://musicbrainz.org/artist/${artistMbid}/wikipedia-extract`, - { - headers: { - Accept: 'application/json', - 'Accept-Language': language, - }, - } - ); + const safeUrl = `https://musicbrainz.org/artist/${artistMbid}/wikipedia-extract`; + + const response = await fetch(safeUrl, { + headers: { + Accept: 'application/json', + 'Accept-Language': language, + 'User-Agent': + 'Jellyseerr/1.0.0 (https://github.com/Fallenbagel/jellyseerr)', + }, + }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -115,7 +126,7 @@ class MusicBrainz extends ExternalAPI { const data = await response.json(); if (!data.wikipediaExtract || !data.wikipediaExtract.content) { - throw new Error('No Wikipedia extract found'); + return null; } const cleanContent = purify.sanitize(data.wikipediaExtract.content, { @@ -129,7 +140,11 @@ class MusicBrainz extends ExternalAPI { content: cleanContent.trim(), }; } catch (error) { - throw new Error(`Error fetching Wikipedia extract: ${error.message}`); + throw new Error( + `[MusicBrainz] Failed to fetch Wikipedia extract: ${ + error instanceof Error ? error.message : 'Unknown error' + }` + ); } } diff --git a/src/components/ArtistDetails/index.tsx b/src/components/ArtistDetails/index.tsx index a4ac63da..462f1cb6 100644 --- a/src/components/ArtistDetails/index.tsx +++ b/src/components/ArtistDetails/index.tsx @@ -360,6 +360,30 @@ const ArtistDetails = () => { async (albumType: string): Promise => { if (!artistId) return; + if ( + !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test( + artistId + ) + ) { + return; + } + + const validAlbumTypes = [ + 'Album', + 'EP', + 'Single', + 'Live', + 'Compilation', + 'Remix', + 'Soundtrack', + 'Broadcast', + 'Demo', + 'Other', + ]; + if (!validAlbumTypes.includes(albumType)) { + return; + } + setAlbumTypes((prev) => ({ ...prev, [albumType]: { @@ -369,10 +393,11 @@ const ArtistDetails = () => { })); try { + const pageSize = Math.min(data?.typeCounts?.[albumType] || 100, 1000); const response = await fetch( - `/api/v1/artist/${artistId}?albumType=${albumType}&pageSize=${ - data?.typeCounts?.[albumType] || 100 - }` + `/api/v1/artist/${artistId}?albumType=${encodeURIComponent( + albumType + )}&pageSize=${pageSize}` ); if (response.ok) { diff --git a/src/components/PersonDetails/index.tsx b/src/components/PersonDetails/index.tsx index 690c117b..f002428f 100644 --- a/src/components/PersonDetails/index.tsx +++ b/src/components/PersonDetails/index.tsx @@ -392,6 +392,31 @@ const PersonDetails = () => { async (albumType: string): Promise => { if (!personId) return; + const parsedPersonId = parseInt(personId, 10); + if ( + isNaN(parsedPersonId) || + parsedPersonId <= 0 || + parsedPersonId.toString() !== personId + ) { + return; + } + + const validAlbumTypes = [ + 'Album', + 'EP', + 'Single', + 'Live', + 'Compilation', + 'Remix', + 'Soundtrack', + 'Broadcast', + 'Demo', + 'Other', + ]; + if (!validAlbumTypes.includes(albumType)) { + return; + } + setAlbumTypes((prev) => ({ ...prev, [albumType]: { @@ -401,9 +426,14 @@ const PersonDetails = () => { })); try { - const pageSize = data?.artist?.typeCounts?.[albumType] || 100; + const pageSize = Math.min( + data?.artist?.typeCounts?.[albumType] || 100, + 1000 + ); const response = await fetch( - `/api/v1/person/${personId}?albumType=${albumType}&pageSize=${pageSize}` + `/api/v1/person/${parsedPersonId}?albumType=${encodeURIComponent( + albumType + )}&pageSize=${pageSize}` ); if (response.ok) { @@ -434,7 +464,7 @@ const PersonDetails = () => { }, })); } - } catch { + } catch (error) { setAlbumTypes((prev) => ({ ...prev, [albumType]: {