source: Klonkt/src/middleware/site.js@ 59e522f

main
Last change on this file since 59e522f was 0beafed, checked in by roboburr <roboburr@…>, 3 months ago

fix: site-wide audio player never received tracks (loadAudioTracks selected m.url, which doesn't exist)

The media table has no url column (verified on demo + sound-fabrics with 91 tracks),
so the query "SELECT m.url AS media_url" always failed silently -> res.locals.audioTracks=[]
-> PCMS_SITE_TRACKS empty -> the persistent site player was blank everywhere. Now
selecting m.filename and building the stream URL via audioUrl(filename), as admin-audio
does. Also fixes the source for the embeddable player (#7).

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

  • Property mode set to 100644
File size: 4.5 KB
Line 
1/**
2 * Site middleware — resolve which site this request is for.
3 *
4 * Resolution order (hub-modus):
5 * 1. Pad /user/:slug → die site (legacy /sites/:slug → 301 naar /user/)
6 * 2. Anders (solo, of hub-landing): de primaire/hoofd-site
7 *
8 * Sets res.locals.site for all downstream handlers.
9 */
10
11import db from '../config/database.js';
12import { getTenancy } from '../services/SettingsService.js';
13import { audioUrl } from '../services/AudioStreamService.js';
14
15/**
16 * De primaire/hoofd-site — ÉÉN bron van waarheid (vervangt de "oudste site ="
17 * hoofd"-aanname die voorheen los in resolveSite/hub/account/admin stond).
18 * Leest de expliciete is_primary-vlag; valt terug op de oudste als die (nog)
19 * nergens staat, zodat bestaand gedrag exact behouden blijft.
20 */
21export function getPrimarySite() {
22 return db.prepare('SELECT * FROM sites WHERE is_primary = 1 LIMIT 1').get()
23 || db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get()
24 || null;
25}
26
27export function resolveSite(req, res, next) {
28 const tenancy = getTenancy();
29 res.locals.tenancy = tenancy; // ook beschikbaar voor views
30
31 // In HUB-mode mapt /user/:slug naar een specifieke site. In SOLO-mode bestaat
32 // er maar één site: we slaan die routing over en pinnen op de primaire site.
33 if (tenancy === 'hub') {
34 // Een Klonkt-site is canoniek bereikbaar via /user/:slug. /sites/:slug is een
35 // legacy-alias → 301 naar de canonieke vorm zodat er één URL-schema overblijft
36 // (behoudt pad + querystring; raakt /admin/sites NIET, dat begint met /admin/).
37 const m = req.path.match(/^\/(sites|user)\/([a-zA-Z0-9_-]+)(\/.*)?$/);
38 if (m) {
39 if (m[1] === 'sites') {
40 return res.redirect(301, req.originalUrl.replace(/^\/sites\//, '/user/'));
41 }
42 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(m[2]);
43 if (site) {
44 res.locals.site = site;
45 req.url = (m[3] || '/'); // strip /user/:slug zodat downstream de rest ziet
46 res.locals.siteUrlBase = `/user/${m[2]}`;
47 return next();
48 }
49 }
50 // (Verwijderd: een dode "slug == hostname"-subdomein-hack. Slugs mogen geen
51 // punten bevatten, dus die kon nooit matchen. Echte subdomein-routing zou de
52 // subdomein-LABEL tegen de slug matchen — een aparte feature, niet dit.)
53 }
54
55 // Solo (of hub zonder match): pin op de primaire/hoofd-site.
56 const defaultSite = getPrimarySite();
57 if (defaultSite) {
58 res.locals.site = defaultSite;
59 res.locals.siteUrlBase = '';
60 }
61
62 next();
63}
64
65/**
66 * Audio tracks loader — pulls site-level tracks for the persistent player widget.
67 * Per Robin: player is separate from the footer, gated by site.enable_audio_player.
68 * Returns empty array if no site or audio is disabled — shell.ejs uses the
69 * length to decide whether to mount audio-player.js.
70 */
71export function loadAudioTracks(req, res, next) {
72 const site = res.locals.site;
73 if (!site || site.enable_audio_player === 0) {
74 res.locals.audioTracks = [];
75 return next();
76 }
77
78 try {
79 // m.filename = de kale bestandsnaam; de speelbare URL is de gated stream-route
80 // (audioUrl). De media-tabel heeft GEEN url-kolom — de oude query selecteerde
81 // m.url en faalde dus altijd stil (lege speler). Nu bouwen we de URL uit filename.
82 const rows = db.prepare(`
83 SELECT t.id, t.title, t.artist, t.duration, t.position, m.filename
84 FROM audio_tracks t
85 LEFT JOIN media m ON m.id = t.media_id
86 WHERE t.site_id = ?
87 ORDER BY t.position ASC, t.created_at ASC
88 `).all(site.id);
89 res.locals.audioTracks = rows.map((r) => ({
90 id: r.id, title: r.title, artist: r.artist, duration: r.duration, position: r.position,
91 media_url: r.filename ? audioUrl(r.filename) : null,
92 }));
93 } catch (e) {
94 // media table might not be queryable in some test setups — fall back gracefully
95 res.locals.audioTracks = [];
96 }
97
98 next();
99}
100
101/**
102 * Theme loader — applies user/site theme preferences.
103 */
104export function loadTheme(req, res, next) {
105 const PALETTES = ['sage','paper','ocean','forest','stone','midnight','sunset','cream'];
106
107 const user = req.session?.user;
108 const site = res.locals.site;
109
110 // Priority: user setting > site setting > default
111 const palette = (user && PALETTES.includes(user.palette) ? user.palette : null)
112 || (site && PALETTES.includes(site.palette) ? site.palette : null)
113 || 'sage';
114
115 res.locals.palette = palette;
116 res.locals.theme = (user && ['dark','light'].includes(user.theme)) ? user.theme : 'dark';
117
118 next();
119}
Note: See TracBrowser for help on using the repository browser.