Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 60 additions & 7 deletions server/components/ISRCSubmission.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div class='action'>
<SpriteIcon name='disc' />
<p>
<MagicISRC allTracks={allTracks} targetMbid={targetMbid} isrcProvider={isrcProvider} />
: Submit ISRCs from <a href={isrcProvider.url}>{isrcProvider.name}</a> to MusicBrainz
</p>
</div>
);
}

function MagicISRC(
{ allTracks, targetMbid, isrcProvider }: {
allTracks: HarmonyTrack[];
targetMbid?: string;
isrcProvider: ProviderInfo;
},
) {
const query = new URLSearchParams(
allTracks.map((track, index) => [`isrc${index + 1}`, track.isrc ?? '']),
);
Expand Down
29 changes: 13 additions & 16 deletions server/routes/release/actions.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,7 +13,6 @@ import type {
ArtistCreditName,
Artwork,
HarmonyRelease,
ProviderInfo,
ReleaseOptions,
ResolvableEntity,
} from '@/harmonizer/types.ts';
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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.
Expand All @@ -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)) {
Expand Down Expand Up @@ -185,14 +184,12 @@ export default defineRoute(async (req, ctx) => {
</p>
</div>
)}
{release && isrcProvider && (
<div class='action'>
<SpriteIcon name='disc' />
<p>
<MagicISRC release={release} targetMbid={releaseMbid!} />
: Submit ISRCs from <a href={isrcProvider.url}>{isrcProvider.name}</a> to MusicBrainz
</p>
</div>
{release && (
<ISRCSubmission
release={release}
targetMbid={releaseMbid}
recordingsCache={mbRecordings}
/>
)}
{releaseUrl && (
<LinkWithMusicBrainz
Expand Down