Changeset b5bae24 in Klonkt
- Timestamp:
- 06/14/2026 01:25:19 AM (3 months ago)
- Branches:
- main
- Children:
- 9b36f45
- Parents:
- c88783e
- Files:
-
- 10 added
- 3 edited
-
.well-known/assetlinks.json (added)
-
deploy/DEPLOY.md (added)
-
deploy/backup.sh (added)
-
deploy/nginx.conf.example (added)
-
deploy/verify.ps1 (added)
-
scripts/import-v9-posts.js (added)
-
scripts/migrate-posts-to-html.js (added)
-
scripts/v9-posts-import.sql (added)
-
src/assets/js/audio-player.js (modified) (5 diffs)
-
src/assets/js/lenis.min.js (added)
-
src/assets/js/smooth-scroll.js (added)
-
src/server.js (modified) (1 diff)
-
src/views/shell.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/audio-player.js
rc88783e rb5bae24 165 165 // earlier fetch resolves. Only the latest load may set audio.src. 166 166 let loadSeq = 0; 167 // Next-track prefetch. While the current track plays we download the *next* 168 // track's bytes into a held blob, so `ended` → next() can swap src instantly 169 // (no silent gap, and no long async window where the browser's autoplay 170 // activation can lapse and reject play()). At most one track ahead is held. 171 // Shape: { url, objUrl } — objUrl is null while the fetch is still in flight. 172 let preload = null; 167 173 168 174 // Hide initially … … 186 192 // Fetch the track bytes and hand back a blob: object URL. The X-Audio-Player 187 193 // header + same-origin credentials get us past the stream route's access gate. 188 async function fetchAsObjectUrl(url) { 189 const r = await fetch(url, { 190 credentials: 'same-origin', 191 headers: { 'X-Audio-Player': '1' }, 192 }); 193 if (!r.ok) throw new Error('HTTP ' + r.status); 194 const blob = await r.blob(); 195 return URL.createObjectURL(blob); 194 // Retries a few times with backoff: a single transient network blip used to 195 // bump the error counter and SKIP the song (auto-advance past it). Now one 196 // hiccup just costs a retry, and we only give up after genuinely failing. 197 async function fetchAsObjectUrl(url, attempts) { 198 attempts = attempts || 1; 199 let lastErr; 200 for (let i = 0; i < attempts; i++) { 201 try { 202 const r = await fetch(url, { 203 credentials: 'same-origin', 204 headers: { 'X-Audio-Player': '1' }, 205 }); 206 if (!r.ok) throw new Error('HTTP ' + r.status); 207 const blob = await r.blob(); 208 return URL.createObjectURL(blob); 209 } catch (e) { 210 lastErr = e; 211 if (i < attempts - 1) { 212 await new Promise((res) => setTimeout(res, 350 * (i + 1))); 213 } 214 } 215 } 216 throw lastErr; 217 } 218 219 // Apply a ready blob URL to the <audio> element. Single source of truth for 220 // "swap the playing source": used by both the cached-preload path and the 221 // fresh-fetch path so there's one place that touches audio.src. 222 function applyBlob(objUrl, autoplay, mySeq) { 223 if (mySeq !== loadSeq) { try { URL.revokeObjectURL(objUrl); } catch (e) {} return; } // superseded 224 root.classList.remove('audio-loading'); 225 // Free the previously-playing track's blob — otherwise each track leaks a 226 // copy. Never the same handle as objUrl (createObjectURL is unique), so this 227 // can't revoke the source we're about to play. 228 if (currentObjectUrl && currentObjectUrl !== objUrl) { 229 try { URL.revokeObjectURL(currentObjectUrl); } catch (e) {} 230 } 231 currentObjectUrl = objUrl; 232 // Schone overgang: pause + load forceert reset van internal state na 233 // meerdere src-changes (voorkomt state-corruption van het audio-element). 234 try { audio.pause(); } catch (e) {} 235 audio.src = objUrl; 236 try { audio.load(); } catch (e) {} 237 if (autoplay) play(); 238 } 239 240 function onLoadError(err, mySeq) { 241 if (mySeq !== loadSeq) return; // superseded — ignore stale failure 242 root.classList.remove('audio-loading'); 243 console.error('[pcms-audio] track load failed after retries', err); 244 // Genuine failure (after retries): bump the counter and auto-skip, but stop 245 // after 3 in a row so a fully-broken queue can't loop "next" forever. 246 consecutiveErrors++; 247 if (consecutiveErrors < 3 && queue.length > 1) setTimeout(next, 400); 248 } 249 250 // Discard any held/in-flight preload and free its blob if resolved. 251 function dropPreload() { 252 if (preload && preload.objUrl) { try { URL.revokeObjectURL(preload.objUrl); } catch (e) {} } 253 preload = null; 254 } 255 256 // Prefetch the *next* track's bytes in the background. Idempotent: re-calling 257 // while the same track is already cached / in flight is a no-op. Called from 258 // the `playing` event so the network is otherwise idle. 259 function preloadNext() { 260 if (queue.length < 2) return; 261 const ni = (currentIndex + 1) % queue.length; 262 const t = queue[ni]; 263 if (!t || !t.url) return; 264 if (preload && preload.url === t.url) return; // already held or in flight 265 dropPreload(); // different track queued before → free it 266 const marker = { url: t.url, objUrl: null }; 267 preload = marker; 268 fetchAsObjectUrl(t.url, 2).then((obj) => { 269 // Only keep it if this is still the track we want next; otherwise free it. 270 if (preload === marker) { marker.objUrl = obj; } 271 else { try { URL.revokeObjectURL(obj); } catch (e) {} } 272 }).catch(() => { if (preload === marker) preload = null; }); 196 273 } 197 274 … … 227 304 228 305 const mySeq = ++loadSeq; 306 307 // Fast path: the bytes for this exact track were already prefetched while 308 // the previous track played → swap in instantly, no gap, no fetch window. 309 if (preload && preload.url === t.url && preload.objUrl) { 310 const obj = preload.objUrl; 311 preload = null; // ownership moves to applyBlob (becomes currentObjectUrl) 312 applyBlob(obj, autoplay, mySeq); 313 return; 314 } 315 316 // Not preloaded (or preload still in flight) → drop any stale preload and 317 // fetch fresh, retrying transient failures before giving up. 318 dropPreload(); 229 319 root.classList.add('audio-loading'); 230 fetchAsObjectUrl(t.url).then((objUrl) => { 231 if (mySeq !== loadSeq) { URL.revokeObjectURL(objUrl); return; } // superseded 232 root.classList.remove('audio-loading'); 233 // Free the previous track's blob — otherwise every track leaks a copy. 234 if (currentObjectUrl) { try { URL.revokeObjectURL(currentObjectUrl); } catch (e) {} } 235 currentObjectUrl = objUrl; 236 // Schone overgang: pause + load forceert reset van internal state na 237 // meerdere src-changes (voorkomt state-corruption van het audio-element). 238 try { audio.pause(); } catch (e) {} 239 audio.src = objUrl; 240 try { audio.load(); } catch (e) {} 241 if (autoplay) play(); 242 }).catch((err) => { 243 if (mySeq !== loadSeq) return; // superseded — ignore stale failure 244 root.classList.remove('audio-loading'); 245 console.error('[pcms-audio] track fetch failed', err); 246 // Treat a failed download like a playback error: bump the counter and 247 // auto-skip, but stop after 3 in a row so we never loop forever. 248 consecutiveErrors++; 249 if (consecutiveErrors < 3 && queue.length > 1) setTimeout(next, 400); 250 }); 320 fetchAsObjectUrl(t.url, 3) 321 .then((objUrl) => applyBlob(objUrl, autoplay, mySeq)) 322 .catch((err) => onLoadError(err, mySeq)); 251 323 } 252 324 … … 298 370 albumName = ''; 299 371 loadSeq++; // cancel any in-flight load 372 dropPreload(); // free any prefetched next-track blob 300 373 if (currentObjectUrl) { try { URL.revokeObjectURL(currentObjectUrl); } catch (e) {} } 301 374 currentObjectUrl = null; … … 345 418 // een kapotte track → infinite "next"-loop. `playing` vuurt alleen als er 346 419 // daadwerkelijk audio speelt. 347 audio.addEventListener('playing', () => { consecutiveErrors = 0; });420 audio.addEventListener('playing', () => { consecutiveErrors = 0; preloadNext(); }); 348 421 audio.addEventListener('pause', () => { isPlaying = false; root.classList.remove('is-playing'); }); 349 422 audio.addEventListener('ended', next); -
src/server.js
rc88783e rb5bae24 218 218 function _renderFavicon(res, accent) { 219 219 const safeAccent = /^#[0-9a-fA-F]{3,8}$/.test(accent) ? accent : '#c2410c'; 220 // Simple PrutFolio mark: a rounded square in the site accent + lowercase 'p' 221 // (display font is server-side unavailable, so we use a generic serif fallback) 220 // Site mark: rounded square in the site accent + bold white 'SF' 222 221 const svg = `<?xml version="1.0" encoding="UTF-8"?> 223 222 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> 224 223 <rect width="64" height="64" rx="14" fill="${safeAccent}"/> 225 224 <text x="50%" y="50%" dy="0.36em" text-anchor="middle" 226 font-family=" Georgia, 'Times New Roman',serif"227 font-size=" 44" font-weight="700" fill="#fff">p</text>225 font-family="Arial, Helvetica, sans-serif" 226 font-size="30" font-weight="800" letter-spacing="-1" fill="#fff">SF</text> 228 227 </svg>`; 229 228 res.set('Content-Type', 'image/svg+xml'); -
src/views/shell.ejs
rc88783e rb5bae24 127 127 <meta name="apple-mobile-web-app-title" content="<%= _e(_siteTitle.slice(0, 16)) %>"> 128 128 <link rel="apple-touch-icon" href="<%= _e(safeSite.profile_photo || '/favicon.ico') %>"> 129 <link rel="icon" type="image/svg+xml" href="/favicon.svg ">130 <link rel="alternate icon" href="/favicon.ico ">129 <link rel="icon" type="image/svg+xml" href="/favicon.svg?v=sf"> 130 <link rel="alternate icon" href="/favicon.ico?v=sf"> 131 131 132 132 <!-- OpenGraph --> … … 277 277 ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat 278 278 Cloudflare (max-age=1y) niet de oude versie blijft serveren. --> 279 <script src="/assets/js/audio-player.js?v= 5"></script>279 <script src="/assets/js/audio-player.js?v=6"></script> 280 280 <% if (site && site.enable_audio_player && audioTracks && audioTracks.length > 0) { %> 281 281 <script>window.PCMS_SITE_TRACKS = <%- JSON.stringify(audioTracks) %>;</script>
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)