Changeset f2eacca in Klonkt
- Timestamp:
- 06/30/2026 01:50:00 AM (2 months ago)
- Branches:
- main
- Children:
- 084d1c0
- Parents:
- 80797d7
- Location:
- src
- Files:
-
- 6 edited
-
config/database.js (modified) (1 diff)
-
routes/admin-audio.js (modified) (2 diffs)
-
routes/audio.js (modified) (2 diffs)
-
services/ActivityPubService.js (modified) (2 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/admin-audio.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r80797d7 rf2eacca 96 96 ensureColumn('audio_tracks', 'link_youtube', 'TEXT'); 97 97 ensureColumn('audio_tracks', 'link_soundcloud', 'TEXT'); 98 // Per-track: federate the actual audio file as an AS2 Audio attachment so it plays inline 99 // in EVERY fediverse client (incl. the Mastodon apps). Default 0 = gated (web player only, 100 // file not exposed). Opt-in 1 = the file is served ungated + shared on the fediverse. 101 ensureColumn('audio_tracks', 'fedi_open', 'INTEGER DEFAULT 0'); 98 102 99 103 // Playlists (v9 feature) — first-class entity. CREATE IF NOT EXISTS is -
src/routes/admin-audio.js
r80797d7 rf2eacca 96 96 const rows = db.prepare(` 97 97 SELECT t.id, t.title, t.artist, t.album, t.duration, t.cover_url, 98 t.position, t.created_at, t.downloadable, m.filename, m.size, m.mime_type98 t.position, t.created_at, t.downloadable, t.fedi_open, m.filename, m.size, m.mime_type 99 99 FROM audio_tracks t 100 100 LEFT JOIN media m ON m.id = t.media_id … … 288 288 }); 289 289 290 // Federate-the-file (fedi_open) per track on/off. When on, this track's audio file is shared 291 // as a real AS2 Audio attachment + served ungated → it plays inline in EVERY fediverse client 292 // (incl. the Mastodon apps), but the file is downloadable. Off (default) = gated, web-player only. 293 router.post('/:id/fedi-open', requireGod, (req, res) => { 294 const site = res.locals.site; 295 if (!site) return res.status(404).send('Site required'); 296 const row = db.prepare('SELECT fedi_open FROM audio_tracks WHERE id = ? AND site_id = ?').get(req.params.id, site.id); 297 if (row) { 298 db.prepare('UPDATE audio_tracks SET fedi_open = ? WHERE id = ? AND site_id = ?') 299 .run(row.fedi_open ? 0 : 1, req.params.id, site.id); 300 } 301 res.redirect('/admin/audio'); 302 }); 303 290 304 router.post('/:id/delete', requireGod, (req, res) => { 291 305 const site = res.locals.site; -
src/routes/audio.js
r80797d7 rf2eacca 54 54 }; 55 55 56 // Access gate: allow only same-origin browser fetches / media loads.57 function isAllowedAudioRequest(req ) {56 // Access gate: same-origin browser fetches / media loads — PLUS fediverse-shared tracks. 57 function isAllowedAudioRequest(req, filename) { 58 58 if (req.get('X-Audio-Player') === '1') return true; // our blob fetch 59 59 const site = req.get('Sec-Fetch-Site'); // set by modern browsers 60 return site === 'same-origin' || site === 'same-site'; 60 if (site === 'same-origin' || site === 'same-site') return true; 61 // fedi_open tracks are deliberately served ungated so remote servers (Mastodon, …) can 62 // fetch + play the file inline. The operator opted this specific track in (per-track flag). 63 if (filename) { 64 try { 65 const r = db.prepare(`SELECT 1 FROM audio_tracks t JOIN media m ON t.media_id = m.id 66 WHERE t.fedi_open = 1 AND (m.storage_path = ? OR m.storage_path LIKE ?) LIMIT 1`).get(filename, '%' + filename); 67 if (r) return true; 68 } catch { /* ignore */ } 69 } 70 return false; 61 71 } 62 72 … … 64 74 const { filename } = req.params; 65 75 66 if (!isAllowedAudioRequest(req )) {76 if (!isAllowedAudioRequest(req, filename)) { 67 77 return res.status(403).send('Direct access not allowed'); 68 78 } -
src/services/ActivityPubService.js
r80797d7 rf2eacca 225 225 for (const m of body.matchAll(/\[\[album:([^\]]+)\]\]/g)) audioLabels.push(m[1].trim()); 226 226 } catch { /* non-fatal */ } 227 // fedi_open tracks → real AS2 Audio attachments (the actual file URL, served ungated) so 228 // EVERY client incl. the Mastodon apps plays them inline natively. Gated tracks (default) 229 // stay link/card-only — the file is never exposed for them. Resolve from post.content so a 230 // later body mutation can't affect it. 231 const openAudio = []; 232 if (hadAudio) { 233 const seenA = new Set(); 234 const addRow = (r) => { 235 const fn = r.filename || (r.storage_path || '').split('/').pop(); 236 if (!fn || seenA.has(fn)) return; seenA.add(fn); 237 openAudio.push({ type: 'Document', mediaType: r.mime_type || 'audio/mpeg', url: `${base}/audio/stream/${encodeURIComponent(fn)}`, name: r.title || 'Audio' }); 238 }; 239 const SEL = 'SELECT t.title, m.filename, m.storage_path, m.mime_type FROM audio_tracks t JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND '; 240 try { 241 for (const mm of (post.content || '').matchAll(/\[\[track:([A-Za-z0-9_-]+)\]\]/g)) { const r = db.prepare(SEL + 't.id = ?').get(mm[1]); if (r) addRow(r); } 242 for (const mm of (post.content || '').matchAll(/\[\[album:([^\]]+)\]\]/g)) for (const r of db.prepare(SEL + 't.site_id = ? AND t.album = ? ORDER BY t.rowid').all(site.id, mm[1].trim())) addRow(r); 243 for (const mm of (post.content || '').matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) for (const r of db.prepare('SELECT t.title, m.filename, m.storage_path, m.mime_type FROM playlist_tracks pt JOIN audio_tracks t ON t.id = pt.track_id JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND pt.playlist_id = ? ORDER BY pt.position').all(mm[1])) addRow(r); 244 } catch { /* non-fatal */ } 245 } 227 246 body = body.replace(/\[\[(track|album|playlist):[^\]]+\]\]/gi, ''); 228 247 // External embeds ([[embed:url]]) → emit the bare URL as a link so Mastodon … … 263 282 .filter((u) => { if (seen.has(u)) return false; seen.add(u); return true; }) 264 283 .map((u) => ({ type: 'Document', mediaType: mediaType(u), url: u })); 284 for (const a of openAudio) attachment.push(a); // fedi_open tracks → native Audio players 265 285 266 286 const note = { -
src/services/i18n.js
r80797d7 rf2eacca 336 336 'aaud.dl_on': 'Download-voor-email staat AAN — klik om uit te zetten', 337 337 'aaud.dl_off': 'Download-voor-email staat uit — klik om aan te zetten', 338 'aaud.fedi_on': 'Op de fediverse gedeeld (speelt overal inline, bestand downloadbaar) — klik om uit te zetten', 339 'aaud.fedi_off': 'Niet op de fediverse gedeeld (alleen webspeler, bestand verborgen) — klik om te delen', 338 340 'aaud.embed_player': 'Embedbare speler', 339 341 'aaud.embed_hint': 'Plak deze code op je eigen website/blog om je muziek met deze speler in te sluiten:', … … 1250 1252 'aaud.dl_on': 'Download-for-email is ON — click to turn off', 1251 1253 'aaud.dl_off': 'Download-for-email is off — click to turn on', 1254 'aaud.fedi_on': 'Shared on the fediverse (plays inline everywhere, file downloadable) — click to turn off', 1255 'aaud.fedi_off': 'Not shared on the fediverse (web player only, file hidden) — click to share', 1252 1256 'aaud.embed_player': 'Embeddable player', 1253 1257 'aaud.embed_hint': 'Paste this code on your own website/blog to embed your music with this player:', … … 2163 2167 'aaud.dl_on': 'Download-für-E-Mail ist AN — zum Ausschalten klicken', 2164 2168 'aaud.dl_off': 'Download-für-E-Mail ist aus — zum Einschalten klicken', 2169 'aaud.fedi_on': 'Im Fediverse geteilt (spielt überall inline, Datei herunterladbar) — zum Ausschalten klicken', 2170 'aaud.fedi_off': 'Nicht im Fediverse geteilt (nur Web-Player, Datei verborgen) — zum Teilen klicken', 2165 2171 'aaud.embed_player': 'Einbettbarer Player', 2166 2172 'aaud.embed_hint': 'Füge diesen Code auf deiner eigenen Website/deinem Blog ein, um deine Musik mit diesem Player einzubetten:', -
src/views/pages/admin-audio.ejs
r80797d7 rf2eacca 83 83 var _aaudDlOn = t('aaud.dl_on'); 84 84 var _aaudDlOff = t('aaud.dl_off'); 85 var _aaudFediOn = t('aaud.fedi_on'); 86 var _aaudFediOff = t('aaud.fedi_off'); 85 87 var _aaudDelete = t('aaud.delete'); 86 88 var _aaudCopy = t('aaud.copy_click'); … … 121 123 style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button> 122 124 <% } %> 125 <form action="/admin/audio/<%= t.id %>/fedi-open" method="post" class="ax-track-fedi" style="display:inline"> 126 <button type="submit" class="ax-icon-btn" 127 aria-label="<%= t.fedi_open ? _aaudFediOn : _aaudFediOff %>" title="<%= t.fedi_open ? _aaudFediOn : _aaudFediOff %>" 128 style="<%= t.fedi_open ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">🌐</button> 129 </form> 123 130 <form action="/admin/audio/<%= t.id %>/delete" method="post" data-confirm="<%= _aaudDelConfirm %>" class="ax-track-delete"> 124 131 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)