diff --git a/server/components/ISRCSubmission.tsx b/server/components/ISRCSubmission.tsx index 6a0ee8b..e63b9c4 100644 --- a/server/components/ISRCSubmission.tsx +++ b/server/components/ISRCSubmission.tsx @@ -1,15 +1,68 @@ +import { SpriteIcon } from './SpriteIcon.tsx'; + import { musicbrainzTargetServer } from '@/config.ts'; -import type { HarmonyRelease } from '@/harmonizer/types.ts'; +import type { HarmonyRelease, HarmonyTrack, ProviderInfo } from '@/harmonizer/types.ts'; import { join } from 'std/url/join.ts'; +import type { EntityWithMbid } from '@kellnerd/musicbrainz/api-types'; -export function MagicISRC({ release, targetMbid }: { release: HarmonyRelease; targetMbid?: string }) { - const allTracks = release.media.flatMap((medium) => medium.tracklist); +// TODO: incomplete type, expose a suitable type from @kellnerd/musicbrainz? +export interface EntityWithISRCs extends EntityWithMbid { + isrcs?: string[]; +} + +function getISRCProvider(release: HarmonyRelease): ProviderInfo | undefined { const isrcSource = release.info.sourceMap?.isrc; - const isrcProvider = release.info.providers.find((provider) => provider.name === isrcSource); - if (!(allTracks.some((track) => track.isrc) && isrcProvider)) { - return null; - } + if (!isrcSource) return undefined; + return release.info.providers.find((provider) => provider.name === isrcSource); +} + +export function ISRCSubmission( + { release, targetMbid, recordingsCache }: { + release: HarmonyRelease; + targetMbid?: string; + recordingsCache?: EntityWithISRCs[]; + }, +) { + const isrcProvider = getISRCProvider(release); + + if (!isrcProvider) return null; + const allTracks = release.media.flatMap((medium) => medium.tracklist); + + const needsSubmission = (track: HarmonyTrack): boolean => { + // If there's no ISRC, nothing to submit + if (!track.isrc) return false; + + const recordingMbid = track.recording?.mbid; + // If there's no recording MBID, consider the ISRC as new (needs submission) + if (!recordingMbid) return true; + + const mbEntity = recordingsCache?.find((e) => e.id === recordingMbid); + // If MB entity is missing or doesn't include the ISRC, it's new + return !mbEntity?.isrcs?.includes(track.isrc); + }; + + const noPendingISRCSubmissions = !allTracks.some(needsSubmission); + + if (noPendingISRCSubmissions) return null; + + return ( +
+ +

+ + : Submit ISRCs from {isrcProvider.name} to MusicBrainz +

+
+ ); +} +function MagicISRC( + { allTracks, targetMbid, isrcProvider }: { + allTracks: HarmonyTrack[]; + targetMbid?: string; + isrcProvider: ProviderInfo; + }, +) { const query = new URLSearchParams( allTracks.map((track, index) => [`isrc${index + 1}`, track.isrc ?? '']), ); diff --git a/server/routes/release/actions.tsx b/server/routes/release/actions.tsx index 91fd4c2..44feb0c 100644 --- a/server/routes/release/actions.tsx +++ b/server/routes/release/actions.tsx @@ -1,6 +1,6 @@ import { ArtistCredit } from '@/server/components/ArtistCredit.tsx'; import { CoverImage } from '@/server/components/CoverImage.tsx'; -import { MagicISRC } from '@/server/components/ISRCSubmission.tsx'; +import { ISRCSubmission } from '@/server/components/ISRCSubmission.tsx'; import { LinkWithMusicBrainz } from '@/server/components/LinkWithMusicBrainz.tsx'; import { MBIDInput } from '@/server/components/MBIDInput.tsx'; import { MessageBox } from '@/server/components/MessageBox.tsx'; @@ -13,7 +13,6 @@ import type { ArtistCreditName, Artwork, HarmonyRelease, - ProviderInfo, ReleaseOptions, ResolvableEntity, } from '@/harmonizer/types.ts'; @@ -37,7 +36,6 @@ export default defineRoute(async (req, ctx) => { let release: HarmonyRelease | undefined = undefined; let releaseMbid: string | undefined; let releaseUrl: URL | undefined; - let isrcProvider: ProviderInfo | undefined; let allArtists: ArtistCreditName[] = []; let mbArtists: EntityWithUrlRels[] = []; let mbLabels: EntityWithUrlRels[] = []; @@ -94,10 +92,6 @@ export default defineRoute(async (req, ctx) => { release.images?.map((image) => ({ ...image, provider })) ?? [] ); - const { info } = release; - const isrcSource = info.sourceMap?.isrc; - isrcProvider = info.providers.find((provider) => provider.name === isrcSource); - const allTracks = release.media.flatMap((medium) => medium.tracklist); // Fallback to track title, Harmony recordings are usually unnamed. @@ -113,9 +107,14 @@ export default defineRoute(async (req, ctx) => { // Load URL relationships for related artists, recordings and labels of the release. // These will be used to skip suggestions to seed external links which already exist. + // For recordings it also includes ISRCs to determine if there are new ones to submit. const mbArtistBrowseResult = await MB.get('artist', { release: releaseMbid, inc: 'url-rels', limit: 100 }); mbArtists = mbArtistBrowseResult.artists; - const mbRecordingBrowseResult = await MB.get('recording', { release: releaseMbid, inc: 'url-rels', limit: 100 }); + const mbRecordingBrowseResult = await MB.get('recording', { + release: releaseMbid, + inc: 'url-rels+isrcs', + limit: 100, + }); mbRecordings = mbRecordingBrowseResult.recordings; // Labels often have no external links which could be linked, save pointless API call. if (release.labels?.some((label) => label.externalIds?.length)) { @@ -185,14 +184,12 @@ export default defineRoute(async (req, ctx) => {

)} - {release && isrcProvider && ( -
- -

- - : Submit ISRCs from {isrcProvider.name} to MusicBrainz -

-
+ {release && ( + )} {releaseUrl && (