source: Klonkt/test/audio-attachment-art.test.js@ a5d14c7

main
Last change on this file since a5d14c7 was 8cf8b5f, checked in by roboburr <roboburr@…>, 2 months ago

feat(fediverse): cover art on federated Audio attachments (AS2 icon)

A fedi_open track's Audio attachment now carries artwork as an AS2 icon (the
track cover, else the post cover), so receivers whose audio player supports
artwork (Mastodon's shows a thumbnail) render it instead of a blank tile. Purely
additive — a receiver that ignores icon is unaffected.

  • src/services/ActivityPubService.js — addRow selects t.cover_url and attaches icon {type:Image, mediaType, url} with post-cover fallback (track + playlist SELECTs).
  • test/audio-attachment-art.test.js — track cover, post-cover fallback, no-cover no-icon.
  • CHANGELOG(.nl/.de).md — "Cover art on openly shared audio" under Unreleased.

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

  • Property mode set to 100644
File size: 2.8 KB
Line 
1// Cover art on federated audio — a fedi_open track's Audio attachment carries the track
2// cover (else the post cover) as an AS2 `icon`, so Mastodon's native audio player can show
3// artwork instead of a blank tile. In-memory SQLite. Run: npm test
4import { test } from 'node:test';
5import assert from 'node:assert/strict';
6
7process.env.DATABASE_PATH = ':memory:';
8process.env.PUBLIC_BASE_URL = 'https://test.example';
9
10const dbMod = await import('../src/config/database.js');
11const db = dbMod.default;
12dbMod.initializeDatabase();
13const AP = (await import('../src/services/ActivityPubService.js')).default;
14
15const BASE = 'https://test.example';
16db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
17db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'demo', 'Demo', 'u1', 1);
18const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
19site.primary_slug = 'demo';
20
21db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
22 .run('m1', 's1', 'song.mp3', 'audio/mpeg', 1000, '/x/song.mp3');
23db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, cover_url, fedi_open) VALUES (?,?,?,?,?,?)')
24 .run('t1', 's1', 'Song One', 'm1', '/media/art.webp', 1);
25db.prepare('INSERT INTO media (id, site_id, filename, mime_type, size, storage_path) VALUES (?,?,?,?,?,?)')
26 .run('m2', 's1', 'bare.mp3', 'audio/mpeg', 1000, '/x/bare.mp3');
27db.prepare('INSERT INTO audio_tracks (id, site_id, title, media_id, fedi_open) VALUES (?,?,?,?,?)')
28 .run('t2', 's1', 'Bare Track', 'm2', 1);
29
30test('a fedi_open track carries its cover as the Audio attachment icon', () => {
31 const post = { id: 'p1', slug: 'x', title: '', content: '<p>[[track:t1]]</p>', tags: '[]', created_at: '2026-01-01T00:00:00Z' };
32 const att = (AP.buildNote(BASE, site, post).attachment || []).find((a) => a.type === 'Audio');
33 assert.ok(att, 'Audio attachment present');
34 assert.ok(att.icon, 'icon present');
35 assert.equal(att.icon.type, 'Image');
36 assert.equal(att.icon.url, `${BASE}/media/art.webp`);
37});
38
39test('a coverless track falls back to the post cover', () => {
40 const post = { id: 'p2', slug: 'y', title: '', content: '<p>[[track:t2]]</p>', tags: '[]', cover_image_url: '/media/post.webp', created_at: '2026-01-01T00:00:00Z' };
41 const att = (AP.buildNote(BASE, site, post).attachment || []).find((a) => a.type === 'Audio');
42 assert.equal(att.icon.url, `${BASE}/media/post.webp`);
43});
44
45test('no track cover and no post cover → no icon', () => {
46 const post = { id: 'p3', slug: 'z', title: '', content: '<p>[[track:t2]]</p>', tags: '[]', created_at: '2026-01-01T00:00:00Z' };
47 const att = (AP.buildNote(BASE, site, post).attachment || []).find((a) => a.type === 'Audio');
48 assert.ok(!('icon' in att));
49});
Note: See TracBrowser for help on using the repository browser.