initial state: muzick music player + recommendation engine
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// 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`);
|
||||
}
|
||||
Reference in New Issue
Block a user