initial state: muzick music player + recommendation engine
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// Deezer API client (read-only).
|
||||
//
|
||||
// Deezer provides a free, no-auth search API with high-quality cover art
|
||||
// (up to 1000×1000 via the `cover_xl` field). Coverage is very good for
|
||||
// European and mainstream releases.
|
||||
//
|
||||
// Rate limit: Deezer doesn't document a hard limit for the public search API,
|
||||
// but ~50 req/min is a safe polite default, enforced 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 Deezer album search result (only the fields we read). */
|
||||
export interface DeezerAlbum {
|
||||
artistName: string;
|
||||
title: string;
|
||||
/** 1000×1000 cover URL. */
|
||||
coverXl: string;
|
||||
/** 500×500 cover URL (fallback). */
|
||||
coverBig: string;
|
||||
}
|
||||
|
||||
interface DeezerSearchResult {
|
||||
artist?: { name?: string };
|
||||
title?: string;
|
||||
cover_xl?: string;
|
||||
cover_big?: string;
|
||||
}
|
||||
|
||||
interface DeezerSearchResponse {
|
||||
data?: DeezerSearchResult[];
|
||||
}
|
||||
|
||||
export class DeezerClient {
|
||||
private readonly baseUrl = 'https://api.deezer.com';
|
||||
private readonly userAgent: string;
|
||||
private readonly minIntervalMs: number;
|
||||
|
||||
constructor(contact = '', minIntervalMs = 1200) {
|
||||
this.userAgent = buildUserAgent(contact);
|
||||
this.minIntervalMs = minIntervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the Deezer catalog for an album by artist + title and return the
|
||||
* best match, or null if no match / error.
|
||||
*
|
||||
* Deezer's search `q` supports the fielded syntax `artist:"…" album:"…"`
|
||||
* which gives precise matches.
|
||||
*/
|
||||
async searchAlbum(artist: string, album: string): Promise<DeezerAlbum | null> {
|
||||
if (artist.trim() === '' || album.trim() === '') return null;
|
||||
|
||||
const q = `artist:"${artist}" album:"${album}"`;
|
||||
const qs = new URLSearchParams({ q });
|
||||
const url = `${this.baseUrl}/search/album?${qs.toString()}`;
|
||||
|
||||
try {
|
||||
const data = await requestJson<DeezerSearchResponse>(url, {
|
||||
userAgent: this.userAgent,
|
||||
minIntervalMs: this.minIntervalMs,
|
||||
});
|
||||
const results = data.data ?? [];
|
||||
if (results.length === 0) return null;
|
||||
|
||||
const best = results[0];
|
||||
const coverXl = best.cover_xl ?? best.cover_big ?? null;
|
||||
const coverBig = best.cover_big ?? best.cover_xl ?? null;
|
||||
if (!coverXl) return null;
|
||||
|
||||
return {
|
||||
artistName: best.artist?.name ?? artist,
|
||||
title: best.title ?? album,
|
||||
coverXl,
|
||||
coverBig: coverBig ?? coverXl,
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[Deezer] searchAlbum failed:', (err as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user