| 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 | // De twee pure vormcontroles wonen in ap-core: ActivityPubService heeft ze ook
|
|---|
| 23 | // nodig voor de actor, en zonder die verhuizing zou dat een KRINGLOOP zijn --
|
|---|
| 24 | // deze module leent immers safeFetch dáár.
|
|---|
| 25 | import { isMbid, artiestUrl } from './ap-core.js';
|
|---|
| 26 |
|
|---|
| 27 | const BASIS = 'https://musicbrainz.org/ws/2';
|
|---|
| 28 |
|
|---|
| 29 | /**
|
|---|
| 30 | * De User-Agent die MusicBrainz eist. Hun regel: naam, versie en een manier om
|
|---|
| 31 | * contact op te nemen. Een lege of generieke string is precies waarop ze
|
|---|
| 32 | * blokkeren, dus als er geen contact is ingesteld zeggen we dat met zoveel
|
|---|
| 33 | * woorden in plaats van iets aardigs te verzinnen.
|
|---|
| 34 | */
|
|---|
| 35 | function userAgent() {
|
|---|
| 36 | const contact = (process.env.MUSICBRAINZ_CONTACT || '').trim()
|
|---|
| 37 | || (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '')
|
|---|
| 38 | || 'geen-contact-ingesteld';
|
|---|
| 39 | return `Klonkt/1.0 ( ${contact} )`;
|
|---|
| 40 | }
|
|---|
| 41 |
|
|---|
| 42 | /**
|
|---|
| 43 | * Hun tempo aanhouden: ten hoogste een verzoek per seconde, over de HELE
|
|---|
| 44 | * applicatie. Geen bibliotheek en geen wachtrij -- een belofte die de volgende
|
|---|
| 45 | * aanroeper laat wachten tot het weer mag. Zonder dit is de eerste drukke dag
|
|---|
| 46 | * meteen een blokkade, en dan werkt het bij iedereen niet meer.
|
|---|
| 47 | */
|
|---|
| 48 | let laatste = 0;
|
|---|
| 49 | let beurt = Promise.resolve();
|
|---|
| 50 | function opDeBeurt() {
|
|---|
| 51 | beurt = beurt.then(async () => {
|
|---|
| 52 | const wachten = 1000 - (Date.now() - laatste);
|
|---|
| 53 | if (wachten > 0) await new Promise((r) => setTimeout(r, wachten));
|
|---|
| 54 | laatste = Date.now();
|
|---|
| 55 | });
|
|---|
| 56 | return beurt;
|
|---|
| 57 | }
|
|---|
| 58 |
|
|---|
| 59 | /**
|
|---|
| 60 | * Zoek artiesten op naam. Geeft de kandidaten met alles wat nodig is om er EEN
|
|---|
| 61 | * uit te kiezen -- de naam alleen is niet genoeg, want er zijn drie bands die
|
|---|
| 62 | * Nirvana heten. Vandaar disambiguation, land en de jaren erbij.
|
|---|
| 63 | *
|
|---|
| 64 | * Geeft een LEGE lijst bij een storing, geen exceptie: niet kunnen zoeken is
|
|---|
| 65 | * vervelend, maar het mag het beheerscherm niet omvergooien.
|
|---|
| 66 | */
|
|---|
| 67 | export async function zoekArtiesten(naam, { limit = 8 } = {}) {
|
|---|
| 68 | const q = String(naam || '').trim();
|
|---|
| 69 | if (!q) return [];
|
|---|
| 70 | const url = `${BASIS}/artist?query=${encodeURIComponent(q)}&fmt=json&limit=${Math.min(25, Math.max(1, limit))}`;
|
|---|
| 71 | try {
|
|---|
| 72 | await opDeBeurt();
|
|---|
| 73 | const r = await safeFetch(url, { headers: { Accept: 'application/json', 'User-Agent': userAgent() } });
|
|---|
| 74 | if (!r || !r.ok) return [];
|
|---|
| 75 | const doc = await r.json();
|
|---|
| 76 | return (doc.artists || []).map(kandidaat).filter(Boolean);
|
|---|
| 77 | } catch {
|
|---|
| 78 | return [];
|
|---|
| 79 | }
|
|---|
| 80 | }
|
|---|
| 81 |
|
|---|
| 82 | /**
|
|---|
| 83 | * Een MBID rechtstreeks opzoeken. Wie zijn id al kent hoeft niet te zoeken --
|
|---|
| 84 | * en een zoekopdracht op een UUID levert bij MusicBrainz niets op, dus zonder
|
|---|
| 85 | * deze tak zou plakken juist het slechtste resultaat geven.
|
|---|
| 86 | */
|
|---|
| 87 | export async function haalArtiest(mbid) {
|
|---|
| 88 | if (!isMbid(mbid)) return null;
|
|---|
| 89 | const url = `${BASIS}/artist/${encodeURIComponent(mbid)}?inc=url-rels&fmt=json`;
|
|---|
| 90 | try {
|
|---|
| 91 | await opDeBeurt();
|
|---|
| 92 | const r = await safeFetch(url, { headers: { Accept: 'application/json', 'User-Agent': userAgent() } });
|
|---|
| 93 | if (!r || !r.ok) return null;
|
|---|
| 94 | return kandidaat(await r.json());
|
|---|
| 95 | } catch {
|
|---|
| 96 | return null;
|
|---|
| 97 | }
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 | /**
|
|---|
| 101 | * DE TERUG-WEG. Noemt de MusicBrainz-pagina van deze artiest ons domein?
|
|---|
| 102 | *
|
|---|
| 103 | * Een koppeling van onze kant is een bewering: iedereen kan een MBID in een
|
|---|
| 104 | * veld typen. Pas als de artiestenpagina TERUGWIJST is het een paar, en dan
|
|---|
| 105 | * weet een lezer dat dezelfde persoon aan allebei de kanten stond. Dat is
|
|---|
| 106 | * dezelfde gedachte als rel="me" bij Mastodon.
|
|---|
| 107 | *
|
|---|
| 108 | * Wij zetten die terugwijzing NIET zelf: via hun API kan het niet, en het hoort
|
|---|
| 109 | * ook niet -- de artiest doet dat op musicbrainz.org onder "social networking".
|
|---|
| 110 | * Wij kijken alleen of hij er staat.
|
|---|
| 111 | *
|
|---|
| 112 | * Geeft { verified, urls } -- bij een storing verified:false en een lege lijst,
|
|---|
| 113 | * want niet kunnen kijken is niet hetzelfde als niet gevonden.
|
|---|
| 114 | */
|
|---|
| 115 | export async function controleerTerugweg(mbid, domein) {
|
|---|
| 116 | const leeg = { verified: false, urls: [] };
|
|---|
| 117 | if (!isMbid(mbid) || !domein) return leeg;
|
|---|
| 118 | let host;
|
|---|
| 119 | try { host = new URL(domein).host.toLowerCase(); } catch { return leeg; }
|
|---|
| 120 | const url = `${BASIS}/artist/${encodeURIComponent(mbid)}?inc=url-rels&fmt=json`;
|
|---|
| 121 | try {
|
|---|
| 122 | await opDeBeurt();
|
|---|
| 123 | const r = await safeFetch(url, { headers: { Accept: 'application/json', 'User-Agent': userAgent() } });
|
|---|
| 124 | if (!r || !r.ok) return leeg;
|
|---|
| 125 | const doc = await r.json();
|
|---|
| 126 | const urls = (doc.relations || [])
|
|---|
| 127 | .map((rel) => rel && rel.url && rel.url.resource)
|
|---|
| 128 | .filter((u) => typeof u === 'string');
|
|---|
| 129 | const wijst = urls.some((u) => { try { return new URL(u).host.toLowerCase() === host; } catch { return false; } });
|
|---|
| 130 | return { verified: wijst, urls };
|
|---|
| 131 | } catch {
|
|---|
| 132 | return leeg;
|
|---|
| 133 | }
|
|---|
| 134 | }
|
|---|
| 135 |
|
|---|
| 136 | /** Een kandidaat, teruggebracht tot wat een mens nodig heeft om te kiezen. */
|
|---|
| 137 | function kandidaat(a) {
|
|---|
| 138 | if (!a || !a.id || !a.name) return null;
|
|---|
| 139 | const jaren = [a['life-span']?.begin, a['life-span']?.ended ? a['life-span']?.end : null]
|
|---|
| 140 | .filter(Boolean).join(' – ');
|
|---|
| 141 | return {
|
|---|
| 142 | mbid: a.id,
|
|---|
| 143 | naam: a.name,
|
|---|
| 144 | // "disambiguation" is het veld waarmee MusicBrainz zelf twee gelijknamige
|
|---|
| 145 | // artiesten uit elkaar houdt. Precies wat de kiezer nodig heeft.
|
|---|
| 146 | toelichting: a.disambiguation || '',
|
|---|
| 147 | soort: a.type || '', // Person, Group, ...
|
|---|
| 148 | land: a.country || '',
|
|---|
| 149 | jaren,
|
|---|
| 150 | url: `https://musicbrainz.org/artist/${a.id}`,
|
|---|
| 151 | // Hun eigen zoekscore. Niet om op te sorteren -- dat doen zij al -- maar om
|
|---|
| 152 | // een zwakke treffer te kunnen tonen als zwak.
|
|---|
| 153 | score: Number(a.score) || 0,
|
|---|
| 154 | };
|
|---|
| 155 | }
|
|---|
| 156 |
|
|---|
| 157 | // Her-geexporteerd zodat een aanroeper er niet over hoeft na te denken waar
|
|---|
| 158 | // ze precies wonen.
|
|---|
| 159 | export { isMbid, artiestUrl };
|
|---|
| 160 | export default { zoekArtiesten, haalArtiest, controleerTerugweg, isMbid, artiestUrl };
|
|---|