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 (
+
+ );
+}
+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 && (
-
+ {release && (
+
)}
{releaseUrl && (