| 1 | /**
|
|---|
| 2 | * Een artiest zoekt zichzelf op in MusicBrainz (shaer-mbz).
|
|---|
| 3 | *
|
|---|
| 4 | * WAAROM DIT GEEN DIALECT IS. Funkwhale's Track/Artist/ArtistCredit zijn hun
|
|---|
| 5 | * eigen vocabulaire -- hun docs noemen ze letterlijk "Custom Funkwhale object"
|
|---|
| 6 | * -- en wij kunnen ze niet eerlijk vullen: artiest en album zijn bij ons
|
|---|
| 7 | * tekstkolommen, geen entiteiten. Een MBID is iets anders: geen vocabulaire
|
|---|
| 8 | * maar een REGISTER. Ernaar verwijzen is als een ISBN noemen. Je neemt niemands
|
|---|
| 9 | * model over en je wijst naar iets dat al bestaat.
|
|---|
| 10 | *
|
|---|
| 11 | * WAT HIER NIET GEBEURT: schrijven. Via hun API zijn alleen tags, ratings,
|
|---|
| 12 | * ISRC's en barcodes in te dienen -- artiesten, releases en recordings niet,
|
|---|
| 13 | * dat gaat via hun website. Wij lezen dus alleen, en dat is meteen de
|
|---|
| 14 | * geruststelling: we kunnen hun register niet vervuilen.
|
|---|
| 15 | *
|
|---|
| 16 | * TWEE HARDE REGELS VAN HUN KANT, allebei hieronder ingebakken omdat ze bij
|
|---|
| 17 | * overtreding tot blokkade leiden en niet tot een foutmelding:
|
|---|
| 18 | * - hoogstens EEN verzoek per seconde, per applicatie (niet per bezoeker)
|
|---|
| 19 | * - een echte User-Agent, met contactgegevens
|
|---|
| 20 | */
|
|---|
| 21 | import { safeFetch } from './ActivityPubService.js';
|
|---|
| 22 |
|
|---|
| 23 | const BASIS = 'https://musicbrainz.org/ws/2';
|
|---|
| 24 |
|
|---|
| 25 | /**
|
|---|
| 26 | * De User-Agent die MusicBrainz eist. Hun regel: naam, versie en een manier om
|
|---|
| 27 | * contact op te nemen. Een lege of generieke string is precies waarop ze
|
|---|
| 28 | * blokkeren, dus als er geen contact is ingesteld zeggen we dat met zoveel
|
|---|
| 29 | * woorden in plaats van iets aardigs te verzinnen.
|
|---|
| 30 | */
|
|---|
| 31 | function userAgent() {
|
|---|
| 32 | const contact = (process.env.MUSICBRAINZ_CONTACT || '').trim()
|
|---|
| 33 | || (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '')
|
|---|
| 34 | || 'geen-contact-ingesteld';
|
|---|
| 35 | return `Klonkt/1.0 ( ${contact} )`;
|
|---|
| 36 | }
|
|---|
| 37 |
|
|---|
| 38 | /**
|
|---|
| 39 | * Hun tempo aanhouden: ten hoogste een verzoek per seconde, over de HELE
|
|---|
| 40 | * applicatie. Geen bibliotheek en geen wachtrij -- een belofte die de volgende
|
|---|
| 41 | * aanroeper laat wachten tot het weer mag. Zonder dit is de eerste drukke dag
|
|---|
| 42 | * meteen een blokkade, en dan werkt het bij iedereen niet meer.
|
|---|
| 43 | */
|
|---|
| 44 | let laatste = 0;
|
|---|
| 45 | let beurt = Promise.resolve();
|
|---|
| 46 | function opDeBeurt() {
|
|---|
| 47 | beurt = beurt.then(async () => {
|
|---|
| 48 | const wachten = 1000 - (Date.now() - laatste);
|
|---|
| 49 | if (wachten > 0) await new Promise((r) => setTimeout(r, wachten));
|
|---|
| 50 | laatste = Date.now();
|
|---|
| 51 | });
|
|---|
| 52 | return beurt;
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | /**
|
|---|
| 56 | * Zoek artiesten op naam. Geeft de kandidaten met alles wat nodig is om er EEN
|
|---|
| 57 | * uit te kiezen -- de naam alleen is niet genoeg, want er zijn drie bands die
|
|---|
| 58 | * Nirvana heten. Vandaar disambiguation, land en de jaren erbij.
|
|---|
| 59 | *
|
|---|
| 60 | * Geeft een LEGE lijst bij een storing, geen exceptie: niet kunnen zoeken is
|
|---|
| 61 | * vervelend, maar het mag het beheerscherm niet omvergooien.
|
|---|
| 62 | */
|
|---|
| 63 | export async function zoekArtiesten(naam, { limit = 8 } = {}) {
|
|---|
| 64 | const q = String(naam || '').trim();
|
|---|
| 65 | if (!q) return [];
|
|---|
| 66 | const url = `${BASIS}/artist?query=${encodeURIComponent(q)}&fmt=json&limit=${Math.min(25, Math.max(1, limit))}`;
|
|---|
| 67 | try {
|
|---|
| 68 | await opDeBeurt();
|
|---|
| 69 | const r = await safeFetch(url, { headers: { Accept: 'application/json', 'User-Agent': userAgent() } });
|
|---|
| 70 | if (!r || !r.ok) return [];
|
|---|
| 71 | const doc = await r.json();
|
|---|
| 72 | return (doc.artists || []).map(kandidaat).filter(Boolean);
|
|---|
| 73 | } catch {
|
|---|
| 74 | return [];
|
|---|
| 75 | }
|
|---|
| 76 | }
|
|---|
| 77 |
|
|---|
| 78 | /** Een kandidaat, teruggebracht tot wat een mens nodig heeft om te kiezen. */
|
|---|
| 79 | function kandidaat(a) {
|
|---|
| 80 | if (!a || !a.id || !a.name) return null;
|
|---|
| 81 | const jaren = [a['life-span']?.begin, a['life-span']?.ended ? a['life-span']?.end : null]
|
|---|
| 82 | .filter(Boolean).join(' – ');
|
|---|
| 83 | return {
|
|---|
| 84 | mbid: a.id,
|
|---|
| 85 | naam: a.name,
|
|---|
| 86 | // "disambiguation" is het veld waarmee MusicBrainz zelf twee gelijknamige
|
|---|
| 87 | // artiesten uit elkaar houdt. Precies wat de kiezer nodig heeft.
|
|---|
| 88 | toelichting: a.disambiguation || '',
|
|---|
| 89 | soort: a.type || '', // Person, Group, ...
|
|---|
| 90 | land: a.country || '',
|
|---|
| 91 | jaren,
|
|---|
| 92 | url: `https://musicbrainz.org/artist/${a.id}`,
|
|---|
| 93 | // Hun eigen zoekscore. Niet om op te sorteren -- dat doen zij al -- maar om
|
|---|
| 94 | // een zwakke treffer te kunnen tonen als zwak.
|
|---|
| 95 | score: Number(a.score) || 0,
|
|---|
| 96 | };
|
|---|
| 97 | }
|
|---|
| 98 |
|
|---|
| 99 | /** Is dit een MBID? Een UUID, en niets anders. */
|
|---|
| 100 | export function isMbid(s) {
|
|---|
| 101 | return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(s || '').trim());
|
|---|
| 102 | }
|
|---|
| 103 |
|
|---|
| 104 | /** De publieke pagina van een artiest, of null als het geen MBID is. */
|
|---|
| 105 | export function artiestUrl(mbid) {
|
|---|
| 106 | return isMbid(mbid) ? `https://musicbrainz.org/artist/${String(mbid).trim().toLowerCase()}` : null;
|
|---|
| 107 | }
|
|---|
| 108 |
|
|---|
| 109 | export default { zoekArtiesten, isMbid, artiestUrl };
|
|---|