- Server: chapters come from yt-dlp's `chapters` (creator chapters and
description timestamps) whenever a video is resolved, kept in a yt_chapters
table; GET /api/chapters?v= serves them (resolving once if needed, 30-day
cache) so server-cached copies get them too. Never fails the player.
- Player: a video's shared chapters win; with none, YouTube's stand in —
listed as "Chapters from YouTube", read-only (no edit/delete; + Chapter
copies them into an editable list). They drive the existing seek-bar ticks
(classic and Glass Stage), the chapter badge and the chapter list, and are
cached offline with the video's notes.
- Keys: [ previous chapter (restarts the current one when >3 s in), ] next.
Profiles without protection behave as before. Protected profiles store only an
argon2id hash; load, save, protection changes and the playlist inbox need the
secret (X-Profile-Secret), wrong guesses are throttled per profile and IP.
Settings: Protect (password or generated key), change/remove, export and import
a credentials file; loading a protected profile prompts for the secret.
- 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).