source: Klonkt/test/ap-fw-track.test.js@ aebde2b

main
Last change on this file since aebde2b was aebde2b, checked in by Robin <roboburr@…>, 3 weeks ago

Album als eigen object op de draad (shaer-756s, stap 2)

De uitgave -- een playlist met kind='album' -- gaat nu als fw:Album mee op
elke track die erop staat: type, id, name, published, released,
musicbrainzId, image en een eigen artist_credit.

INGESLOTEN EN NIET ALS URI, en dat is de kern. Hun TrackSerializer heeft
album = AlbumSerializer(): een object met eigen verplichte velden. Een
kale URI expandeert naar een knoop met alleen een @id en valt daar af --
precies waarom Emissary's tracks bij Funkwhale net zo goed stranden,
terwijl ze het veld wel sturen. Als URI staat het album daarnaast op het
Audio-object zelf, zoals Funkwhale 2.0 en Emissary allebei doen.

Het id is de BESTAANDE playlist-collectie. Geen tweede adres voor een
ding dat er al een heeft, en die URI draagt nu dezelfde albumvelden zodat
er hetzelfde staat als in het ingesloten object.

type op die collectie blijft OrderedCollection, en blijft een STRING.
Er stond hier even ['OrderedCollection','Album'] -- geldig AS2, en het is
ook werkelijk allebei -- maar een bestaande test viel erover en had
gelijk: een lezer die type als tekst uitpakt (Shaer) verliest dan in
stilte de hele playlist. Het kost niets, want hun AlbumSerializer
declareert geen type-veld en valideert het dus niet.

trackAlbums() zoekt in EEN query op, zoals trackHostPosts: per track
vragen wordt bij tweehonderd nummers tweehonderd queries. Alleen
kind='album' telt mee -- een mixtape is geen uitgave, en zou hij hier wel
een album opleveren dan was dat onderscheid decoratie.

artistCredit() is nu gedeeld door track en album. Twee keer los bouwen is
hoe een nummer en de plaat waar hij op staat een verschillende artiest
krijgen.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 10.7 KB
Line 
1// `fw:track` op onze Audio-objecten (shaer-3f8a, spoor B stap 1).
2//
3// Twee onafhankelijke implementaties lezen dit veld: Funkwhale (verplicht in
4// UploadSerializer) en Emissary 0.9.0, gemeten op bandwagon.fm 16-8. Emissary
5// stuurt de kleine vorm -- type, id, name, album, position -- en die nemen we
6// over, zonder album zolang dat bij ons geen object is (shaer-k37k).
7//
8// Wat deze test vastlegt is vooral het ID: het nummer krijgt een EIGEN
9// identiteit met #track, niet die van het bestand. Emissary hergebruikt daar
10// het object-id, en dan zijn de Audio en de Track in JSON-LD een knoop met twee
11// typen. Wie dat 'opruimt' naar het kortere id haalt dat verschil weg, en dan
12// staat er dat een mp3 een muziekstuk IS.
13import { test } from 'node:test';
14import assert from 'node:assert/strict';
15
16process.env.DATABASE_PATH = ':memory:';
17process.env.PUBLIC_BASE_URL = 'https://ons.test';
18
19const dbMod = await import('../src/config/database.js');
20const db = dbMod.default;
21dbMod.initializeDatabase();
22const AP = await import('../src/services/ActivityPubService.js');
23
24const BASE = 'https://ons.test';
25db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
26 .run('u1', 'u1', 'u1@t', 'x', 'god');
27db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
28 .run('s1', 'band', 'De Band', 'u1');
29const site = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
30
31db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,?)')
32 .run('m1', 's1', 'een.mp3', 'a/een.mp3', 'audio/mpeg', 2480880);
33db.prepare('INSERT INTO audio_tracks (id, site_id, title, artist, duration, media_id, fedi_open, position) VALUES (?,?,?,?,?,?,?,?)')
34 .run('t1', 's1', 'Het nummer', 'De Band', 103, 'm1', 1, 3);
35// Eentje zonder positie: die mag het veld niet verzinnen.
36db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,?)')
37 .run('m2', 's1', 'twee.mp3', 'a/twee.mp3', 'audio/mpeg', 100);
38db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, fedi_open) VALUES (?,?,?,?,?)')
39 .run('t2', 's1', 'Zonder plek', 'm2', 1);
40
41const audio = (id) => AP.buildTrackAudio(BASE, site, AP.openTrack('s1', id), { hostPosts: null });
42
43test('elke Audio draagt een track met de kleine Emissary-vorm', () => {
44 const a = audio('t1');
45 assert.ok(a.track, 'geen track op het object');
46 assert.equal(a.track.type, 'Track');
47 assert.equal(a.track.name, 'Het nummer');
48 assert.equal(a.track.position, 3);
49});
50
51test('de track heeft een EIGEN id, niet dat van het bestand', () => {
52 const a = audio('t1');
53 assert.equal(a.track.id, `${a.id}#track`);
54 assert.notEqual(a.track.id, a.id, 'een bestand is geen werk');
55});
56
57test('een track zonder uitgave draagt GEEN album', () => {
58 // Dit stond er in stap 1 als "nog geen album, want we hebben er geen object
59 // voor". Sinds stap 2 hebben we dat wel -- de album-playlist -- en is de regel
60 // scherper: geen uitgave, geen veld. De tekstkolom `album` op de track blijft
61 // buiten de draad, want dat is een label en geen adres.
62 db.prepare("INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES ('m9','s1','los.mp3','a/los.mp3','audio/mpeg',10)").run();
63 db.prepare("INSERT INTO audio_tracks (id, site_id, title, album, media_id, fedi_open) VALUES ('t9','s1','Losse track','Een Albumnaam','m9',1)").run();
64 const a = audio('t9');
65 assert.equal(a.track.album, undefined, 'een albumNAAM is geen album');
66 assert.equal(a.album, undefined);
67});
68
69test('geen verzonnen positie', () => {
70 assert.equal(audio('t2').track.position, undefined);
71 assert.equal(audio('t2').track.name, 'Zonder plek');
72});
73
74test('de termen staan in de context, anders valt het veld weg', async () => {
75 const core = await import('../src/services/ap-core.js');
76 const term = core.AP_CONTEXT.find((x) => x && typeof x === 'object' && x.track);
77 assert.ok(term, 'track staat in de context');
78 // Letterlijk de vorm uit hun contexts.py (regel 293-306), zodat een lezer die
79 // hun context laadt op dezelfde IRI's uitkomt als een lezer die de onze leest.
80 assert.deepEqual(term.track, { '@id': 'fw:track', '@type': '@id' });
81 assert.equal(term.Track, 'fw:Track');
82});
83
84test('de track komt overal mee, niet alleen op de losse ophaal', () => {
85 // Bibliotheek, trackcollectie en losse ophaal bouwen alle drie via
86 // buildTrackAudio -- dit is de test die dat vasthoudt als iemand er een
87 // eigen bouwertje naast zet.
88 const lib = AP.buildLibrary(BASE, site, AP.siteOpenTracks('s1'));
89 for (const a of lib.items) assert.equal(a.track.type, 'Track', `${a.name} zonder track`);
90 const col = AP.buildTrackCollection(BASE, site, AP.siteOpenTracks('s1'));
91 for (const a of col.orderedItems) assert.equal(a.track.type, 'Track', `${a.name} zonder track`);
92});
93
94// ── artist_credit (shaer-3f8a, het gat naar hun ingest) ──────────────
95//
96// TrackSerializer (regel 1569) eist `artist_credit` met min_length=1, en elke
97// ArtistCredit eist een Artist die zelf id, name en published nodig heeft
98// (MusicEntitySerializer, regel 1278). Dat leek onmogelijk zolang een artiest
99// bij ons tekst was -- tot de MusicBrainz-koppeling liet zien dat de ACTOR de
100// artiest is.
101
102test('de track draagt een artist_credit met een echte Artist', () => {
103 const a = audio('t1');
104 assert.ok(Array.isArray(a.track.artist_credit), 'geen lijst');
105 assert.equal(a.track.artist_credit.length, 1);
106 const ac = a.track.artist_credit[0];
107 assert.equal(ac.type, 'ArtistCredit');
108 assert.ok(ac.id && ac.published, 'ArtistCredit mist id of published');
109 assert.equal(ac.artist.type, 'Artist');
110 assert.equal(ac.artist.id, `${BASE}/ap/users/band`, 'de actor IS de artiest');
111 assert.equal(ac.artist.name, 'De Band');
112 assert.ok(ac.artist.published, 'Artist mist published');
113});
114
115test('de artiestnaam van de track gaat naar credit, niet naar de entiteit', () => {
116 // De kolom is een credittekst, geen identiteit. Er een id van maken zou
117 // dezelfde fout zijn als bij het album (shaer-756s).
118 assert.equal(audio('t1').track.artist_credit[0].credit, 'De Band');
119 assert.equal(audio('t2').track.artist_credit[0].credit, undefined, 'geen artiest, geen credit');
120});
121
122test('de track heeft published -- MusicEntitySerializer eist het', () => {
123 assert.ok(audio('t1').track.published);
124});
125
126test('artist_credit is een @list in de context, anders vindt hun lezer niets', async () => {
127 // Ze lezen dit met first_attr(FW.artist_credit, "@list"). Zonder de
128 // container-declaratie expandeert onze array niet naar een @list en staat er
129 // iets dat er goed uitziet en niet gevonden wordt.
130 const core = await import('../src/services/ap-core.js');
131 const term = core.AP_CONTEXT.find((x) => x && typeof x === 'object' && x.artist_credit);
132 assert.deepEqual(term.artist_credit, { '@id': 'fw:artist_credit', '@type': '@id', '@container': '@list' });
133 assert.equal(term.ArtistCredit, 'fw:ArtistCredit');
134 assert.equal(term.Artist, 'fw:Artist');
135});
136
137test('musicbrainzId komt mee zodra de site gekoppeld is', () => {
138 assert.equal(audio('t1').track.artist_credit[0].artist.musicbrainzId, undefined);
139 db.prepare("UPDATE sites SET mb_artist_id = '5441c29d-3602-4898-b1a1-b77fa23b8e50' WHERE id = 's1'").run();
140 const s2 = db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
141 const a = AP.buildTrackAudio(BASE, s2, AP.openTrack('s1', 't1'), { hostPosts: null });
142 assert.equal(a.track.artist_credit[0].artist.musicbrainzId, '5441c29d-3602-4898-b1a1-b77fa23b8e50');
143 db.prepare("UPDATE sites SET mb_artist_id = NULL WHERE id = 's1'").run();
144});
145
146// ── het album ingesloten (shaer-756s, stap 2) ────────────────────────
147//
148// Hun TrackSerializer heeft `album = AlbumSerializer()`: een OBJECT met name,
149// published en een eigen artist_credit. Een kale URI expandeert naar een knoop
150// met alleen een @id en valt daar af -- dat is precies waarom Emissary's tracks
151// bij Funkwhale ook stranden.
152
153db.prepare(`INSERT INTO playlists (id, site_id, title, artist, year, cover_url, kind, release_date, mb_release_id, created_at)
154 VALUES ('de-plaat','s1','De Plaat','De Band',2024,'/media/hoes.jpg','album','2024-03-15','7c5a9b2e-1111-4222-8333-944455556666','2026-01-01T00:00:00Z')`).run();
155db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('de-plaat','t1',1)").run();
156// t2 zit in een MIXTAPE en hoort dus geen album te krijgen.
157db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('de-mix','s1','De Mix','playlist')").run();
158db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('de-mix','t2',1)").run();
159
160test('een track op een uitgave draagt het album als OBJECT', () => {
161 const a = audio('t1');
162 const al = a.track.album;
163 assert.ok(al && typeof al === 'object', 'album is geen object');
164 assert.equal(al.type, 'Album');
165 assert.equal(al.id, `${BASE}/ap/users/band/playlists/de-plaat`, 'het id is de bestaande collectie');
166 assert.equal(al.name, 'De Plaat');
167 assert.ok(al.published, 'MusicEntitySerializer eist published');
168 assert.equal(al.released, '2024-03-15');
169 assert.equal(al.musicbrainzId, '7c5a9b2e-1111-4222-8333-944455556666');
170 assert.ok(Array.isArray(al.artist_credit) && al.artist_credit.length >= 1, 'album zonder artist_credit');
171 assert.equal(al.image.type, 'Image');
172 // Ook als URI op de Audio zelf -- Funkwhale 2.0 en Emissary doen dat allebei.
173 assert.equal(a.album, al.id);
174});
175
176test('een track in een MIXTAPE krijgt geen album', () => {
177 // Een afspeellijst is geen uitgave. Zou hij hier een album krijgen, dan was
178 // het onderscheid album/playlist decoratie.
179 const a = audio('t2');
180 assert.equal(a.track.album, undefined);
181 assert.equal(a.album, undefined);
182});
183
184test('track en album delen dezelfde artiest', () => {
185 // Twee keer los bouwen is hoe ze uit elkaar gaan lopen; er is een functie.
186 const a = audio('t1');
187 assert.deepEqual(a.track.artist_credit[0].artist, a.track.album.artist_credit[0].artist);
188});
189
190test('de playlist-collectie draagt de albumvelden, met type als STRING', () => {
191 const pl = db.prepare("SELECT * FROM playlists WHERE id = 'de-plaat'").get();
192 const col = AP.buildPlaylistCollection(BASE, site, pl, AP.playlistOpenTracks('de-plaat'));
193 assert.equal(typeof col.type, 'string', 'een array breekt lezers die type als tekst uitpakken');
194 assert.equal(col.type, 'OrderedCollection');
195 assert.equal(col.released, '2024-03-15');
196 assert.equal(col.musicbrainzId, '7c5a9b2e-1111-4222-8333-944455556666');
197 assert.ok(col.artist_credit);
198});
199
200test('een mixtape-collectie krijgt GEEN albumvelden', () => {
201 const pl = db.prepare("SELECT * FROM playlists WHERE id = 'de-mix'").get();
202 const col = AP.buildPlaylistCollection(BASE, site, pl, AP.playlistOpenTracks('de-mix'));
203 assert.equal(col.released, undefined);
204 assert.equal(col.artist_credit, undefined);
205});
Note: See TracBrowser for help on using the repository browser.