initial state: muzick music player + recommendation engine

This commit is contained in:
kami
2026-07-14 01:35:52 +04:00
commit 737bf19fd1
196 changed files with 32431 additions and 0 deletions
+98
View File
@@ -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`);
}