Commit Graph

288 Commits

Author SHA1 Message Date
Jonathan Sykes
796293c0a5 Admin upload card: styled file pickers, aligned fields, drag-and-drop
The raw browser file inputs (grey 'Choose File', truncated 'No fi...osen')
sat beside styled fields of other heights with Upload orphaned on its own row.
Now a drop zone that names the chosen file (size, type) and accepts drops, a
labelled Title / Artist / Cover row with a cover thumbnail preview, and a
clear 'choose a file first' message. Same IDs and upload code; stacks on phones.
2026-10-03 07:37:22 +08:00
Jonathan Sykes
47feded91b Sidebar: hide the Classic SVG nav icons outside the Classic layout
They relied on the hidden attribute, which browsers do not reliably honour on
SVG elements, so in Glass Stage each nav item drew a full-size black icon.
2026-10-03 07:29:38 +08:00
Jonathan Sykes
5154ef5f75 Collect discovered video metadata and recommend videos from listening history 2026-10-03 07:16:51 +08:00
Jonathan Sykes
343eb53d89 Redesign Classic layout for desktop and mobile 2026-10-03 06:44:22 +08:00
Jonathan Sykes
54fcd12119 YouTube chapters: extracted automatically, ticks on the seek bar and Glass scrubber, [ / ] to jump
- 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.
2026-10-03 03:52:50 +08:00
Jonathan Sykes
8084411f6a Bulk-select a channel's videos into a playlist; last card no longer hidden under the bottom bars
- Channel pages join the select-mode views: tap Select in the filter row, pick
  videos (or Select all — respects the filter), then + Queue / + Playlist.
  Delete is hidden there (nothing to delete on a channel).
- --bottom-chrome (the fixed bottom nav + mini player height) is now measured
  whenever the mini player shows/hides and on resize/rotation, not only while
  the section rail scrolls, and the list pane pads its end by it, so the last
  card scrolls fully clear with or without the mini player.
2026-10-03 02:21:55 +08:00
Jonathan Sykes
2dfce8a2fb iPhone/iPad: no page zoom; PCs keep browser zoom
Stray pinches and double-taps kept zooming the installed app. On iOS only the
viewport gets maximum-scale=1/user-scalable=no, Safari's gesture events are
cancelled and the page uses touch-action: manipulation. The player's own pinch
zoom (pointer events) still works.
2026-10-03 02:07:41 +08:00
Jonathan Sykes
9ce9a96f38 Channels load for every channel type; parallel saves setting (default 4)
Channels: one loader tries, in order, the Videos/Live/Shorts tabs (normal
channels), the uploads playlist (Topic channels), the Releases/Playlists tabs
expanded into their tracks (artist/music channels whose only content is albums,
e.g. Cathedral of Praise Worship), then the home page; bare names are resolved
to an id first. Nothing public → ok with a message, never a raw yt-dlp error.

Saves: SaveSlots caps concurrent offline saves (Settings → Offline cache →
Videos saved at once, 1–8, default 4); queued saves show "Waiting for a free
slot", and resumed saves now run up to the limit instead of one at a time.
2026-10-03 02:04:27 +08:00
Jonathan Sykes
71f1913d0b Optional profile password or key, with a credentials file to sign in other devices
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.
2026-10-03 01:51:07 +08:00
Jonathan Sykes
91627dc0a9 Share target: shared YouTube links play, shared audio/video uploads to the user's library; page shortcuts
- 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).
2026-10-03 01:33:51 +08:00
Jonathan Sykes
6581509ddd Update icon beside the search box after the update dialog is dismissed
Shown as soon as an update is offered, so Later, a backdrop tap or a failed
download all leave a one-tap way to update; it targets the newest build and
disappears while updating. The search form and box now shrink so the icon
never pushes Search off a phone screen.
2026-10-03 01:26:52 +08:00
Jonathan Sykes
65f5325a8e Search from service mode no longer strands the user on the home screen
In service mode's Video view the overlay lives inside the player pane, which
the home-over rule hid, while service mode kept the bottom bar and mini bar
hidden. Going home now leaves service mode first, and the rule spares the
service overlay.
2026-10-03 01:15:44 +08:00
Jonathan Sykes
caf0199c11 Fast lyric fitting for long videos; Search tab returns to the home screen while playing
- fitLyricLines measures text on a canvas with the line's own font and writes
  every --fit after measuring (it used an in-list DOM probe and wrote inside
  the loop, re-laying-out the whole list per line). 1200 lines: 600 ms -> 28 ms;
  sizes match the old method within 1 px. Service mode on long sermons with
  hundreds of transcribed lines opened slowly enough on phones to look broken.
- Bottom-nav Search while something plays shows the home screen (playlist
  board / hero) over the player pane; the media keeps playing unseen and the
  mini bar brings the player back (its close button too, so there is always a
  way back). Starting another video restores the player.
2026-10-03 01:10:15 +08:00
Jonathan Sykes
171a0bd127 Glass Stage buffered chip: 'Saved on device' for device copies, and measure the range around the playhead
It read the last buffered range, so seeking ahead made the figure jump, and a
device copy showed a meaningless partial percentage.
2026-10-03 01:00:09 +08:00
Jonathan Sykes
cbffa1fd8b Saves: paint progress only on Downloads rows, not on search/playlist cards
Every card of a video carries .downloading while it saves, so the progress
painter rewrote their channel line and its final repaint left 'Saving for
offline…' on the card after the save had finished.
2026-10-03 00:59:18 +08:00
Jonathan Sykes
16e2b24c31 Resumable saves: prepare reports the mp4's real size, not the cache row's mp4+m4a total
The media_cache row's size counts the audio sidecar too, so a device asked for
bytes past the end of the file and could never finish a ranged save.
2026-10-03 00:49:24 +08:00
Jonathan Sykes
56b3f4e449 Resumable saves: ranged downloads that survive dropped connections, reloads and app kills
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.
2026-10-03 00:46:52 +08:00
Jonathan Sykes
c8601c5953 Merge remote-tracking branch 'github/claude/friendly-dirac-hz5pje' into resume-downloads 2026-10-03 00:28:45 +08:00
Jonathan Sykes
11ddb8a970 Uploads library on the USB drive with a nightly stable-disk copy; Topic channels resolve by name
- UPLOAD_DIR moves to the USB drive; reads fall back to UPLOAD_BACKUP_DIR on the
  ytplayer-data volume, and while the drive is offline new uploads land there
- scripts/ops/uploads-backup.sh (homelab cron, 03:30) copies both ways, never
  deletes; the server removes a deleted upload from both places
- /api/channel resolves a bare channel name ('Artist - Topic') to its id via a
  search, and the client falls back to the channel name for old saved entries
2026-10-02 22:38:05 +08:00
Jonathan Sykes
1f9a97a6ae Media cache on the homelab USB drive with an offline guard; Topic channels list their tracks
- MEDIA_DIR moves to /mnt/data/ytplayer (bind-mounted at /app/bulk); DB, lyrics,
  notes and uploads stay on the ytplayer-data volume
- MEDIA_VOLUME_MARKER: while the marker on the drive is missing the cache writes,
  serves and deletes nothing, so playback streams and an unmounted drive never
  fills the disk under its mountpoint or loses its index on a reboot
- Budget 250 GiB, keep 20 GiB free on the drive
- /api/channel falls back to the uploads playlist and the channel home page when
  a channel has no Videos tab (auto-generated '- Topic' music channels)
2026-10-02 21:21:23 +08:00
Jonathan Sykes
53da71ceb9 Service mode: one view button (default, lyrics, video, visualizer, album art) and a text-size box
- A single dock button cycles the service view; video is skipped in audio-only mode
- Visualizer reacts to the audio where the EQ graph is available and falls back
  to an ambient pulse on iOS so background playback is never routed through Web Audio
- Album art shows the song artwork with the letterbox cropped
- A-/A+ box steps lyrics, title and channel size from -2 to +2 in every theme
- Glass Stage dock is slightly more compact
2026-10-02 21:11:15 +08:00
Jonathan Sykes
985063648f Icon-only playlist header and filter-row actions, one-row transport, resumable update download
- Playlist title wraps within 90% of the width; rename and share are icons beside it,
  and tapping the title opens the rename box showing the full name
- Offline, pin, select and delete are icon buttons to the right of the filter input
- Transport is one row again: prev, back 10, play, forward 10, next
- Update download stages each file as it arrives so a retry only fetches what is
  missing, keeps 4 requests in flight and allows 5 minutes
2026-10-02 20:59:51 +08:00
Jonathan Sykes
236c129b90 Pin PyAV below 19 so faster-whisper can open audio again 2026-10-02 19:19:20 +08:00
Claude
0d92accd1d Run SHA-256 as WebAssembly with JS fallback; note ffmpeg.wasm as deferred 2026-10-02 11:10:51 +00:00
Jonathan Sykes
3f8c9fc365 Stop the lyrics worker from filling the disk: skip long audio, sweep stale temp files, back off after a crash 2026-10-02 15:06:10 +08:00
Claude
883c9e7ba0 Document search library, server search cache and versioned static caching 2026-10-02 02:15:36 +00:00
Claude
ee248d2303 IndexedDB search library (1000x100, 15-day copies, Update search), server video metadata for instant local results 2026-10-02 02:15:28 +00:00
Claude
1b10a994dd Version-stamped static URLs cached forever, thumbnail cache 25000 with throttled trimming 2026-10-02 02:11:38 +00:00
Claude
c69cc3a4bc Persistent server search cache: 200 results per query, 250k queries, 15-day stale-while-revalidate, refresh=1 2026-10-02 02:10:39 +00:00
Claude
14e2307a92 Keep search results with thumbnails until cleared in Settings; add stale-while-revalidate search and longer asset caching 2026-10-01 22:04:51 +00:00
Claude
61a8f63385 Make the channel name a visible link directly under the title 2026-10-01 21:54:39 +00:00
Claude
fecb824c9a Style the A-B group, Fullscreen and More buttons like the mockup 2026-10-01 21:54:17 +00:00
Claude
8c463f3834 Rebuild the Glass Stage A-B quick row and restyle the title block with a verified badge 2026-10-01 21:51:38 +00:00
Claude
6bf2636864 Bring Glass Stage closer to the mockup: HUD chips, sleep card, chapter badge, A-B group, playlist banner and P2P strip 2026-10-01 21:45:44 +00:00
Claude
19973c75c0 Add the Glass Stage layout theme with overlay controls, 10 s skips, quick chips and stacked tiles 2026-10-01 21:40:20 +00:00
Claude
36fc5ff9e4 Merge Queue and Playlist into one Add button and put Service mode in the common actions 2026-10-01 21:17:37 +00:00
Claude
859ecd2255 Move speaker and playback mode beside the volume slider with a volume-aware icon 2026-10-01 21:13:27 +00:00
Claude
9be4844b48 Show only the dots on More controls while A-B markers are present 2026-10-01 21:09:59 +00:00
Claude
b3a43c66db Label the fullscreen button, add a mini fullscreen icon and show A-B markers beside it 2026-10-01 21:09:00 +00:00
Claude
25e7a28b6a Merge shuffle, repeat list and loop one into one playback mode button 2026-10-01 21:03:48 +00:00
Claude
f9cb7edeb2 Replace transport emoji with centred SVG icons and distinguish loop-one from repeat-list 2026-10-01 21:02:23 +00:00
Claude
9f313f61af Move secondary player controls into a More controls sheet 2026-10-01 20:58:15 +00:00
Claude
88f878b8eb Move rarely used video actions into a More actions sheet 2026-10-01 20:55:04 +00:00
Claude
47826536a6 Make the refresh button 10% of the row width 2026-10-01 20:43:41 +00:00
Claude
9eb5e98048 Add Gallery action beside Save and an icon-only refresh beside Play all 2026-10-01 20:39:17 +00:00
Claude
02b337fcbc Add save-to-device export, server export route and an On other devices P2P list 2026-10-01 18:53:00 +00:00
Claude
08e95814e5 Support hour-long uploads and videos: raise body limit, stream uploads to disk, cache up to 3 h, add deferred-ideas note 2026-10-01 18:48:37 +00:00
Claude
9d74769029 Add deck tabs, uniform action buttons, lyric reports in the player panel and a song list in the lyrics studio 2026-09-30 15:57:11 +00:00
Claude
e64880c163 Redesign playlist header, player deck, now-playing actions and lyrics panel layout 2026-09-30 15:29:44 +00:00
Claude
3fb1d29d5a Redesign admin as a tabbed lyrics studio with undo, tap-sync pad and reported-line triage 2026-09-30 15:21:11 +00:00