- frontend/wasm/loudness.c → loudness-wasm.js (1.5 KB, base64-embedded; built by
scripts/build-loudness-wasm.sh with clang or Zig's clang): K-weighting + 100 ms
energy steps. loudness.js does the 400 ms gating (abs -70 LUFS, rel -10 LU)
with an identical JS fallback. EBU Tech 3341 sine cases measure -23.0/-33.0
on both engines; a real track reads -21.7 vs ffmpeg ebur128 -21.8.
- The device measures from audio it already has (saved copy or the server
cache's m4a sidecar, songs up to 12 min, decoded natively at 22.05 kHz) and
reports the number to /api/loudness, so each song is measured once for
everyone; the server stores numbers only, no audio processing.
- The EQ graph gains a level stage (gain -> limiter) that eases each track
toward -14 LUFS (-12..+9 dB). Settings -> Playback -> Level volume: on by
default on Android/desktop, opt-in on iPhone (Web Audio stops sound when the
screen locks there).
- manifest share_target (POST multipart: title/text/url + audio/video files)
and eight shortcuts (Search, Queue, Saved, Downloads, History, Library,
Notes, Settings). /?view=<page> now actually opens that page — the old
shortcuts pointed at it but nothing read it.
- sw.js receives the share: files are parked in the ytp-share-inbox cache (not
a versioned shell cache, so deploys never evict it) and the app is opened;
links/text go to /?shared=. A server POST /share-target fallback covers the
first visit before a worker is in control (links only).
- The app plays a YouTube link found anywhere in the shared text (watch,
youtu.be, shorts, live, embed, music; keeps t=), otherwise searches the text.
/?shared=<link> also works from an iOS Shortcut (iOS has no share target).
- Shared files upload with progress to PUT /api/uploads/shared and join a
"Shared uploads" playlist; network failures stay in the inbox for the next
open, refusals are dropped with the reason.
- The public route is bounded: audio/video only (ffprobe-validated, error text
without server paths), 500 MB per file, 2 GB and 20 a day per device, 50 GB
for all shared uploads, one at a time per device (all env-tunable). Shared
uploads are unlisted: reachable by id, never in anyone else's search.
uploads gains owner + listed columns (idempotent ALTER).
Server
- /api/download/:id answers Range with a strong ETag ("<id>.<gen>") and honours
If-Range; a stale partial gets the whole current file (streamed, so Bun does
not re-apply the Range itself). Uploads get the same treatment.
- GET /api/download/:id/prepare never blocks: ready {gen,size,sha256,etag,ext},
working (server still fetching), legacy (HEVC / too long / cache offline),
failed. Recently refused prepares are remembered for 10 minutes.
Browser
- The OPFS worker saves in 8 MiB ranges, writes at the byte offset, flushes
each chunk, retries each chunk 6 times with backoff (30 s idle timeout) and
keeps the .part plus a .part.json sidecar naming the server copy it belongs
to. A changed copy restarts cleanly; the finished file is hashed once and
checked against the server's SHA-256.
- SaveQueue remembers unfinished saves and resumes them on start, online,
return to the foreground and every 2 minutes while visible; one at a time.
- Downloads shows live MB progress, 'Preparing on server', 'Verifying', and
paused saves with Resume and Cancel (confirmed).
- listVideos ignores the sidecars; new listPartials/discardPartial helpers.
Verified in Chromium through a connection-dropping proxy: paused at 8 MiB,
auto-resumed after a reload from byte 8388608, final SHA-256 matched.
When auto-advancing through a queue or playlist — or stepping with
next/prev — the next video previously jumped to its saved resume
timestamp. A freshly selected track now starts at the beginning, or at
the A marker when an A-B loop is set for it. Resuming still applies when
you reopen a single video directly.
A-B markers set while playing from a playlist are now stored on that
playlist's own copy of the video (entry.ab), so each playlist keeps its
own loop and the markers sync to the database alongside the playlist.
Non-playlist playback keeps using the global per-video marker map.
Bumps service worker to v1.0.3 to bust the client cache.
Existing PWA installs cache the app shell (app.js et al.) cache-first under
ytplayer-v1.0.0. Without a version bump they'd keep serving the old app.js
that lacked WEB-mode OPFS support, so the offline-cache fix would never reach
already-installed clients. Bumping forces activate → old cache delete →
re-fetch, and triggers the in-app 'Update ready' banner.