Files
muzick/workers/src/integrations/itunes.client.ts
T

99 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// iTunes Search API client (read-only).
//
// The iTunes Search API is free, requires no API key, and has excellent
// coverage — nearly every released album is present. We use the album search
// to resolve cover art URLs. The default artwork URL returned is 100×100, but
// can be upgraded to 600×600 by swapping the size suffix in the URL string.
//
// Rate limit: Apple asks for "moderate" use (~20 req/min). We enforce a polite
// per-host interval via the http base.
//
// Best-effort: on any error, methods log a warning and return null rather than
// throwing, so enrichment never breaks the worker.
import { buildUserAgent, requestJson } from './http.js';
/** A normalised iTunes album search result (only the fields we read). */
export interface ITunesAlbum {
/** Artist canonical name. */
artistName: string;
/** Album / collection title. */
collectionName: string;
/** Cover art URL (100×100 — caller upscales). */
artworkUrl100: string;
}
interface ITunesSearchResult {
artistName?: string;
collectionName?: string;
artworkUrl100?: string;
}
interface ITunesSearchResponse {
results?: ITunesSearchResult[];
}
export class ITunesClient {
private readonly baseUrl = 'https://itunes.apple.com';
private readonly userAgent: string;
private readonly minIntervalMs: number;
constructor(contact = '', minIntervalMs = 3000) {
this.userAgent = buildUserAgent(contact);
this.minIntervalMs = minIntervalMs;
}
/**
* Search the iTunes catalog for an album by artist + title and return the
* best match, or null if no match / error. The returned `artworkUrl100` is
* the 100×100 thumbnail; callers upscale it to 600×600 via `upscaleArtwork`.
*/
async searchAlbum(artist: string, album: string): Promise<ITunesAlbum | null> {
if (artist.trim() === '' || album.trim() === '') return null;
// The iTunes search `term` is a free-text query; we quote both values to
// narrow the match. entity=album restricts to album collections.
const term = `${artist} ${album}`;
const qs = new URLSearchParams({ term, entity: 'album', limit: '5' });
const url = `${this.baseUrl}/search?${qs.toString()}`;
try {
const data = await requestJson<ITunesSearchResponse>(url, {
userAgent: this.userAgent,
minIntervalMs: this.minIntervalMs,
});
const results = data.results ?? [];
if (results.length === 0) return null;
// Prefer an exact title match (case-insensitive) to avoid grabbing a
// "Greatest Hits" when searching for a studio album, then fall back to
// the first result.
const lowerAlbum = album.toLowerCase();
const exact = results.find(
(r) => r.collectionName?.toLowerCase() === lowerAlbum
);
const best = exact ?? results[0];
if (!best.artworkUrl100) return null;
return {
artistName: best.artistName ?? artist,
collectionName: best.collectionName ?? album,
artworkUrl100: best.artworkUrl100,
};
} catch (err) {
console.warn('[iTunes] searchAlbum failed:', (err as Error).message);
return null;
}
}
}
/**
* Upscale an iTunes 100×100 artwork URL to a larger size. iTunes serves
* artwork from a CDN that supports arbitrary sizes via the `NNNxNNN` path
* segment (e.g. 600x600). Returns the input unchanged if it doesn't match the
* expected shape.
*/
export function upscaleITunesArtwork(url: string, size = 600): string {
return url.replace(/\/\d+x\d+(bb)?\.(jpg|png)$/, `/${size}x${size}bb.$2`);
}