Something went wrong. Try again.
A decentralized music tracking and discovery platform built on AT Protocol ๐ต rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164import type { DiscogsArtistCredit, DiscogsLabelRef, DiscogsMaster, DiscogsRelease,} from "./discogs";import type { InsertDiscogsIdentifier } from "schema/discogs-identifiers";import type { InsertDiscogsMaster } from "schema/discogs-masters";import type { InsertDiscogsReleaseArtist } from "schema/discogs-release-artists";import type { InsertDiscogsReleaseLabel } from "schema/discogs-release-labels";import type { InsertDiscogsTrack } from "schema/discogs-tracks";
// Pure mappers from the service's Discogs objects to the relational rows. They// take the release row id so the caller owns the write.
const trimmed = (value?: string | null) => value?.trim() || null;
// Mirrors the service's own parsing so a stored track carries the same numbers// the enrichment reports: "7", "2-04" (disc 2) and "C2" (vinyl side C, the// second disc of a 2xLP). Vinyl numbering stays side-relative, as printed.export const parsePosition = ( position: string,): [disc: number, track: number] => { const pos = position.trim(); if (!pos) { return [0, 0]; } const split = pos.match(/^(\d+)[-.](\d+)/); if (split) { return [Number(split[1]), Number(split[2])]; } const vinyl = pos.match(/^([A-Za-z])(\d+)/); if (vinyl) { const side = vinyl[1].toUpperCase().charCodeAt(0) - "A".charCodeAt(0); return [Math.floor(side / 2) + 1, Number(vinyl[2])]; } return /^\d+$/.test(pos) ? [0, Number(pos)] : [0, 0];};
// Discogs writes "m:ss" or "h:mm:ss".export const parseDurationMs = (duration: string): number => { const parts = duration.trim().split(":"); if (parts.length < 1 || parts.length > 3 || duration.trim() === "") { return 0; } let seconds = 0; for (const part of parts) { const n = Number(part.trim()); if (!Number.isInteger(n) || n < 0) { return 0; } seconds = seconds * 60 + n; } return seconds * 1000;};
// Rebuilds the credit string from Discogs' join phrases.export const joinArtistCredits = (artists: DiscogsArtistCredit[] = []) => artists .filter((artist) => (artist.anv || artist.name)?.trim()) .reduce((credit, artist, i, list) => { const name = (artist.anv || artist.name).trim(); if (i === 0) { return name; } const join = list[i - 1].join?.trim(); const separator = !join || join === "," ? ", " : ` ${join} `; return credit + separator + name; }, "");
export const toReleaseArtistRows = ( releaseId: string, artists: DiscogsArtistCredit[] = [],): InsertDiscogsReleaseArtist[] => artists .filter((artist) => artist.name?.trim()) .map((artist, position) => ({ releaseId, artistId: artist.id || null, name: artist.name.trim(), anv: trimmed(artist.anv), joinPhrase: trimmed(artist.join), role: trimmed(artist.role), position, }));
// Labels and companies land in one table, told apart by `kind`.export const toReleaseLabelRows = ( releaseId: string, release: Pick<DiscogsRelease, "labels" | "companies">,): InsertDiscogsReleaseLabel[] => { const rows = (entries: DiscogsLabelRef[] = [], kind: "label" | "company") => entries .filter((entry) => entry.name?.trim()) .map((entry, position) => ({ releaseId, labelId: entry.id || null, name: entry.name.trim(), catalogNumber: trimmed(entry.catno), kind, entityType: trimmed(entry.entity_type_name) ?? trimmed(entry.entity_type), position, })); return [ ...rows(release.labels, "label"), ...rows(release.companies, "company"), ];};
export const toIdentifierRows = ( releaseId: string, identifiers: DiscogsRelease["identifiers"] = [],): InsertDiscogsIdentifier[] => identifiers .filter((identifier) => identifier.type?.trim()) .map((identifier, position) => ({ releaseId, type: identifier.type.trim(), value: trimmed(identifier.value), description: trimmed(identifier.description), position, }));
export const toTrackRows = ( releaseId: string, tracklist: DiscogsRelease["tracklist"] = [],): InsertDiscogsTrack[] => tracklist .filter((entry) => entry.title?.trim()) .map((entry, idx) => { const [disc, track] = parsePosition(entry.position ?? ""); const ms = parseDurationMs(entry.duration ?? ""); return { releaseId, position: trimmed(entry.position), type: trimmed(entry.type_), title: entry.title.trim(), duration: trimmed(entry.duration), durationMs: ms > 0 ? ms : null, discNumber: disc > 0 ? disc : null, trackNumber: track > 0 ? track : null, idx, }; });
export const toMasterRow = ( master: DiscogsMaster,): InsertDiscogsMaster | undefined => { if (!master.id || !master.title?.trim()) { return undefined; } return { discogsId: master.id, title: master.title.trim(), artist: joinArtistCredits(master.artists) || null, year: master.year || null, mainReleaseId: master.main_release || null, discogsUrl: trimmed(master.uri), genres: master.genres?.length ? master.genres : null, styles: master.styles?.length ? master.styles : null, };};