- Timestamp:
- 06/20/2026 05:23:08 AM (3 months ago)
- Branches:
- main
- Children:
- d727e92
- Parents:
- 459acd9
- Location:
- src
- Files:
-
- 8 edited
-
assets/css/audio.css (modified) (1 diff)
-
config/database.js (modified) (1 diff)
-
routes/admin-audio.js (modified) (6 diffs)
-
routes/posts.js (modified) (4 diffs)
-
services/AudioEmbedService.js (modified) (4 diffs)
-
services/PlaylistService.js (modified) (2 diffs)
-
views/partials/track-editor.ejs (modified) (2 diffs)
-
views/shell.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/audio.css
r459acd9 r183875b 82 82 text-overflow: ellipsis; 83 83 } 84 85 /* "Open in" Spotify/YouTube/SoundCloud — kleine brand-iconen per track. */ 86 .pat-links { 87 display: inline-flex; align-items: center; gap: 0.1rem; 88 flex: 0 0 auto; 89 } 90 /* Single + album-rij (.post-audio-track is flex) → duw de links naar rechts. */ 91 .post-audio-track .pat-links { margin-left: auto; padding-right: 0.5rem; } 92 /* Playlist-rij: de li flex maken zodat de links naast de afspeel-rij passen. */ 93 .post-album-track-compact { display: flex; align-items: center; } 94 .post-album-track-compact .pat-row { flex: 1 1 auto; min-width: 0; } 95 .post-album-track-compact .pat-links { padding-right: 0.6rem; } 96 .pat-link { 97 display: inline-flex; align-items: center; justify-content: center; 98 width: 26px; height: 26px; 99 opacity: 0.75; 100 transition: opacity 120ms, transform 120ms; 101 } 102 .pat-link svg { width: 17px; height: 17px; } 103 .pat-link:hover { opacity: 1; transform: scale(1.12); } 104 .pat-link--spotify { color: #1DB954; } 105 .pat-link--youtube { color: #FF0000; } 106 .pat-link--soundcloud { color: #FF5500; } 84 107 85 108 /* ============================================================ -
src/config/database.js
r459acd9 r183875b 92 92 ensureColumn('audio_tracks', 'credit', 'TEXT'); // eigenaar/credit (copyright-houder) 93 93 ensureColumn('audio_tracks', 'license', 'TEXT'); // licentie (bv. "CC BY 4.0", "Alle rechten voorbehouden") 94 ensureColumn('audio_tracks', 'link_spotify', 'TEXT'); // "open in"-links per track 95 ensureColumn('audio_tracks', 'link_youtube', 'TEXT'); 96 ensureColumn('audio_tracks', 'link_soundcloud', 'TEXT'); 94 97 95 98 // Playlists (v9 feature) — first-class entity. CREATE IF NOT EXISTS is -
src/routes/admin-audio.js
r459acd9 r183875b 72 72 73 73 const router = express.Router(); 74 75 // "Open in"-platformlinks per track: alleen https + de juiste host accepteren 76 // (href komt ongeescaped in de view → scheme/host-guard tegen misbruik). 77 const LINK_DOMAINS = { 78 spotify: ['spotify.com'], 79 youtube: ['youtube.com', 'youtu.be', 'music.youtube.com'], 80 soundcloud: ['soundcloud.com'], 81 }; 82 function platformLink(url, domains) { 83 const u = String(url || '').trim(); 84 if (!u || !/^https:\/\//i.test(u)) return null; 85 try { 86 const h = new URL(u).hostname.toLowerCase(); 87 if (domains.some((d) => h === d || h.endsWith('.' + d))) return u; 88 } catch (e) { /* ongeldige URL */ } 89 return null; 90 } 74 91 75 92 router.get('/', requireGod, (req, res) => { … … 172 189 const finalCredit = (req.body.credit || '').trim() || finalArtist || null; 173 190 const finalLicense = (req.body.license || '').trim() || null; 191 const finalLinkSpotify = platformLink(req.body.link_spotify, LINK_DOMAINS.spotify); 192 const finalLinkYoutube = platformLink(req.body.link_youtube, LINK_DOMAINS.youtube); 193 const finalLinkSoundcloud = platformLink(req.body.link_soundcloud, LINK_DOMAINS.soundcloud); 174 194 175 195 console.log('[admin-audio] upload received:', { … … 222 242 console.log('[admin-audio] inserting audio_tracks row (duration=' + finalDuration + ')'); 223 243 db.prepare(` 224 INSERT INTO audio_tracks (id, site_id, title, artist, album, duration, cover_url, credit, license, media_id, position)225 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(244 INSERT INTO audio_tracks (id, site_id, title, artist, album, duration, cover_url, credit, license, link_spotify, link_youtube, link_soundcloud, media_id, position) 245 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE( 226 246 (SELECT MAX(position) + 1 FROM audio_tracks WHERE site_id = ?), 227 247 0 … … 233 253 coverUrl, 234 254 finalCredit, finalLicense, 255 finalLinkSpotify, finalLinkYoutube, finalLinkSoundcloud, 235 256 mediaId, site.id 236 257 ); … … 378 399 const t = db.prepare(` 379 400 SELECT t.id, t.title, t.artist, t.album, t.duration, t.cover_url, 380 t.credit, t.license, t.position, t.created_at, m.filename 401 t.credit, t.license, t.link_spotify, t.link_youtube, t.link_soundcloud, 402 t.position, t.created_at, m.filename 381 403 FROM audio_tracks t LEFT JOIN media m ON m.id = t.media_id 382 404 WHERE t.id = ? AND t.site_id = ? … … 445 467 if (Object.prototype.hasOwnProperty.call(body, 'license')) { 446 468 fields.push('license = ?'); values.push(String(body.license || '').trim() || null); 469 } 470 if (Object.prototype.hasOwnProperty.call(body, 'link_spotify')) { 471 fields.push('link_spotify = ?'); values.push(platformLink(body.link_spotify, LINK_DOMAINS.spotify)); 472 } 473 if (Object.prototype.hasOwnProperty.call(body, 'link_youtube')) { 474 fields.push('link_youtube = ?'); values.push(platformLink(body.link_youtube, LINK_DOMAINS.youtube)); 475 } 476 if (Object.prototype.hasOwnProperty.call(body, 'link_soundcloud')) { 477 fields.push('link_soundcloud = ?'); values.push(platformLink(body.link_soundcloud, LINK_DOMAINS.soundcloud)); 447 478 } 448 479 -
src/routes/posts.js
r459acd9 r183875b 529 529 const placeholders = trackIds.map(() => '?').join(','); 530 530 const rows = db.prepare(` 531 SELECT t.id, t.title, t.artist, t.cover_url, t.credit, t.license, m.filename 531 SELECT t.id, t.title, t.artist, t.cover_url, t.credit, t.license, 532 t.link_spotify, t.link_youtube, t.link_soundcloud, m.filename 532 533 FROM audio_tracks t LEFT JOIN media m ON m.id = t.media_id 533 534 WHERE t.site_id = ? AND t.id IN (${placeholders}) … … 544 545 credit: r.credit || '', 545 546 license: r.license || '', 547 link_spotify: r.link_spotify || '', 548 link_youtube: r.link_youtube || '', 549 link_soundcloud: r.link_soundcloud || '', 546 550 url: audioUrl(r.filename), 547 551 }; … … 554 558 const placeholders = albumNames.map(() => '?').join(','); 555 559 const albumRows = db.prepare(` 556 SELECT t.id, t.title, t.artist, t.album, t.cover_url, t.position, m.filename 560 SELECT t.id, t.title, t.artist, t.album, t.cover_url, t.position, 561 t.link_spotify, t.link_youtube, t.link_soundcloud, m.filename 557 562 FROM audio_tracks t LEFT JOIN media m ON m.id = t.media_id 558 563 WHERE t.site_id = ? AND t.album IN (${placeholders}) … … 569 574 artist: r.artist || '', 570 575 cover: r.cover_url || '', 576 link_spotify: r.link_spotify || '', 577 link_youtube: r.link_youtube || '', 578 link_soundcloud: r.link_soundcloud || '', 571 579 }); 572 580 } -
src/services/AudioEmbedService.js
r459acd9 r183875b 11 11 */ 12 12 13 // "Open in"-iconen (brand-gekleurd via CSS .pat-link--*). 14 const OPEN_IN_SVG = { 15 spotify: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 100 20 10 10 0 000-20zm4.6 14.42a.62.62 0 01-.86.21c-2.35-1.44-5.3-1.76-8.79-.96a.62.62 0 11-.28-1.21c3.8-.87 7.07-.5 9.71 1.11.3.18.39.57.22.85zm1.23-2.73a.78.78 0 01-1.07.26c-2.69-1.66-6.79-2.14-9.97-1.17a.78.78 0 11-.45-1.49c3.63-1.1 8.15-.56 11.24 1.33.36.22.48.7.25 1.07zm.1-2.85C14.66 8.95 9.4 8.78 6.3 9.72a.93.93 0 11-.54-1.79c3.56-1.08 9.37-.87 13.07 1.33a.94.94 0 01-.96 1.61z"/></svg>', 16 youtube: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 7.1a3 3 0 00-2.1-2.12C19.04 4.5 12 4.5 12 4.5s-7.04 0-8.9.48A3 3 0 001 7.1 31.2 31.2 0 00.5 12 31.2 31.2 0 001 16.9a3 3 0 002.1 2.12c1.86.48 8.9.48 8.9.48s7.04 0 8.9-.48A3 3 0 0023 16.9 31.2 31.2 0 0023.5 12 31.2 31.2 0 0023 7.1zM9.75 15.5v-7l6 3.5-6 3.5z"/></svg>', 17 soundcloud: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 14v4M7.5 11v7M11 9v9"/><path d="M14.5 9.5V18h4a3 3 0 100-6 4 4 0 00-4-2.5z"/></svg>', 18 }; 19 13 20 class AudioEmbedService { 21 // Kleine "open in"-links voor een track (Spotify/YouTube/SoundCloud). De hrefs 22 // zijn server-side al gevalideerd (alleen https + juiste host). Geeft '' als er 23 // geen links zijn. Wordt naast de play-knop gezet (buiten de knop → geen 24 // conflict met afspelen). 25 static openInLinks(t) { 26 if (!t) return ''; 27 const out = []; 28 const add = (url, key, label) => { 29 if (!url) return; 30 out.push(`<a class="pat-link pat-link--${key}" href="${this.escape(url)}" target="_blank" rel="noopener noreferrer" title="Open in ${label}" aria-label="Open in ${label}">${OPEN_IN_SVG[key]}</a>`); 31 }; 32 add(t.link_spotify, 'spotify', 'Spotify'); 33 add(t.link_youtube, 'youtube', 'YouTube'); 34 add(t.link_soundcloud, 'soundcloud', 'SoundCloud'); 35 return out.length ? `<span class="pat-links">${out.join('')}</span>` : ''; 36 } 37 14 38 static detectProvider(url) { 15 39 if (!url || typeof url !== 'string') return null; … … 273 297 ${creditBits ? `<div class="pat-credit">${creditBits}</div>` : ''} 274 298 </div> 299 ${this.openInLinks(t)} 275 300 </div>`; 276 301 }); … … 312 337 ${tArtist && tArtist !== artistH ? `<div class="pat-artist">${tArtist}</div>` : ''} 313 338 </div> 339 ${this.openInLinks(t)} 314 340 </li>`; 315 341 }).join('\n'); … … 433 459 ${durHtml} 434 460 </button> 461 ${this.openInLinks(t)} 435 462 </li>`; 436 463 }).join('\n'); -
src/services/PlaylistService.js
r459acd9 r183875b 94 94 // filenames (only tracks with a media file are playable). 95 95 const tracks = db.prepare(` 96 SELECT t.id, t.title, t.artist, t.duration, t.cover_url, m.filename 96 SELECT t.id, t.title, t.artist, t.duration, t.cover_url, 97 t.link_spotify, t.link_youtube, t.link_soundcloud, m.filename 97 98 FROM playlist_tracks pt 98 99 JOIN audio_tracks t ON t.id = pt.track_id … … 110 111 cover: t.cover_url || p.cover_url || '', 111 112 duration: t.duration || 0, 113 link_spotify: t.link_spotify || '', 114 link_youtube: t.link_youtube || '', 115 link_soundcloud: t.link_soundcloud || '', 112 116 url: urlFor ? urlFor(t.filename) : null, 113 117 })); -
src/views/partials/track-editor.ejs
r459acd9 r183875b 430 430 431 431 <div class="te-field"> 432 <span>Open in <small>(links naar dezelfde track elders)</small></span> 433 <input type="url" id="te-link-spotify" inputmode="url" autocomplete="off" spellcheck="false" 434 placeholder="Spotify-URL (https://open.spotify.com/…)" value="${esc(track.link_spotify || '')}"> 435 <input type="url" id="te-link-youtube" inputmode="url" autocomplete="off" spellcheck="false" 436 placeholder="YouTube-URL (https://youtu.be/…)" value="${esc(track.link_youtube || '')}"> 437 <input type="url" id="te-link-soundcloud" inputmode="url" autocomplete="off" spellcheck="false" 438 placeholder="SoundCloud-URL (https://soundcloud.com/…)" value="${esc(track.link_soundcloud || '')}"> 439 </div> 440 441 <div class="te-field"> 432 442 <span>Cover</span> 433 443 <div class="te-cover-row"> … … 664 674 credit: $('#te-credit').value.trim() || null, 665 675 license: $('#te-license').value.trim() || null, 676 link_spotify: $('#te-link-spotify').value.trim() || null, 677 link_youtube: $('#te-link-youtube').value.trim() || null, 678 link_soundcloud: $('#te-link-soundcloud').value.trim() || null, 666 679 duration: $('#te-duration').value ? Number($('#te-duration').value) : null, 667 680 cover_url: urlInput.value.trim() || null, -
src/views/shell.ejs
r459acd9 r183875b 168 168 anywhere (admin previews, post embeds, etc). The player itself is 169 169 a singleton — see the script tag near </body>. --> 170 <link rel="stylesheet" href="/assets/css/audio.css?v= 7">170 <link rel="stylesheet" href="/assets/css/audio.css?v=8"> 171 171 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) in huisstijl. --> 172 172 <link rel="stylesheet" href="/assets/css/embed.css?v=8">
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)