Commit Graph
7 Commits
Author SHA1 Message Date
vincent 62110b28fc fix: subtitles no longer depend on the Settings preference
Verified in the running app: captions render on a downloaded video and
on a stream, and the player's menu lists them under SUBTITLES beside
AUDIO.

Three things were wrong.

The preference decided whether subtitles were fetched at all, so "Off"
— the old default, which nobody had to choose — silently emptied the
subtitle menu as well. Fetching now happens regardless; the preference
only says which language switches itself on.

Choosing a track from the menu moved the preference from "off" to that
language, which re-ran the fetch, tore down the tracks and stalled the
stream mid-play. The fetch keys on the language it would ask for, not
on the preference, and that value does not change when "off" becomes
the language it already stood for.

The preference was re-applied on every poll of the track list, so a
choice made in the menu was undone a second later. It now applies once
per set of tracks, which also makes "Off" mean off.

Anyone still carrying the old "off" default is moved to English once.
An Off chosen deliberately after this is left alone.
2026-08-29 16:46:34 +02:00
vincent 5b2be50852 feat: subtitles while streaming, and fetched when a download lacks them
YouTube's HLS manifest carries a dozen audio renditions and no
subtitles whatsoever — checked against a live master playlist: 16
EXT-X-MEDIA entries, all TYPE=AUDIO, zero SUBTITLES. So the track menu
could only ever list audio while streaming, which is what it did.

Subtitles are now fetched separately with yt-dlp, tidied through the
existing VTT cleanup, and cached per video and language. They reach the
player as blob URLs, which share the document's origin — a file:// or
127.0.0.1 track would be cross-origin to the page and need CORS the
media pipeline cannot supply. The same path fills in a download saved
before subtitles were switched on, without fetching the video again.

YouTube serves identical auto-generated captions under both "en" and
"en-orig", so byte-identical texts collapse to one entry rather than
offering the same track twice, and tracks are labelled "English"
rather than "en".

The subtitle preference now defaults to English. "None" is a poor
default for a setting whose whole purpose is captions: it silently
means no subtitles are downloaded, fetched, or offered anywhere, and
the Settings text now says so.
2026-08-29 16:25:26 +02:00
vincent 7aae080e46 feat: durations, clean subtitles, icon buttons
Subtitles rendered pinned to the left edge and clipped, in half-grey
karaoke text: YouTube's auto-captions carry align:start position:0% on
every cue plus inline <00:00:12.480><c>word</c> timing tags. Both are
now stripped after download, and existing downloads are tidied the first
time they are listed.

Thumbnails show video length. The Atom feed carries no duration, so it
is read from the watch page and cached — but only a trickle. The first
version fetched 24 pages every 12 seconds at six concurrent, roughly two
requests a second sustained, and YouTube answered by challenging the
whole IP: 'Sign in to confirm you are not a bot', which broke streaming
and downloads too. It is now four pages every five minutes, one at a
time, and stops for the session the moment a batch is refused.

A subtitle chosen in the player becomes the stored preference, so the
next video matches without going back to Settings.

The back and download buttons are square icon buttons; the back glyph
was off-centre because px-2 beat the p-0 meant to clear it.
2026-08-29 14:07:17 +02:00
vincent 557467baad feat: subtitles, delete-all, auto refresh, and menu cleanup
Subtitles: a language preference in Settings drives what is shown and
what is downloaded. yt-dlp saves WebVTT sidecars next to each download,
including YouTube's auto-generated track, and the player attaches them
as <track> elements so they work offline. Sidecars are removed with
their video, and by Delete all.

WebVTT rather than muxed subtitle streams because WebKit reads a <track>
reliably and largely ignores subtitle tracks inside an MP4.

Downloads in progress now appear under the downloaded-only filter, so a
download you just started does not vanish from the list you are watching
it in. That view also gains a Delete all, behind a confirmation naming
what goes.

The feed refreshes on launch and whenever the player closes, so the
Refresh button is only for staleness.

Player: Delete moved to the footer and shortened, Open on YouTube is now
an external-link icon.

Removes the Edit and Help menus. Cut/Copy/Paste move to the app menu,
without which their shortcuts would stop working in the search field.
The webview context menu is suppressed outside text fields — its Reload
and Back items act on a page the app does not present as one.

Settings now warns that 4K AV1 plays back with artefacts: the files
decode cleanly in ffmpeg, so it is the built-in decoder, not the
download.
2026-08-29 13:25:17 +02:00
vincent 73f12cfac1 fix: default to original audio, and pick tracks from the transport bar
YouTube marks an AI-dubbed audio track DEFAULT=YES on some videos and
AVFoundation obeys the manifest, so a synthetic voice played over the
original. The app now always serves its own rewritten master playlist —
previously only when a quality cap was set — and marks the track whose
language matches the video's own as the default, falling back to
whichever track does not describe itself as dubbed. Every track is still
listed, so switching remains possible.

Adds an audio and subtitle picker to the transport bar, reading the
video element's audioTracks and textTracks. Subtitles start off for the
same reason the dub does: nothing translated is forced on.

Also collapses the player's own title-bar strip in window fullscreen,
which was leaving a stray bar above the player header.
2026-08-29 12:54:05 +02:00
vincent 9bb7b71225 feat: uniform control height, chrome auto-hide, streaming quality
Every control in the app now shares one height (CONTROL_H), with width
still growing to fit its label. The player transport is larger, Back is
an icon, and the footer buttons match the prev/next size.

Player chrome — transport and edge arrows — fades after 2.6s of
inactivity and never hides while paused.

The title-bar strip collapses in macOS window fullscreen, where the
traffic lights are gone and it was just a blank white bar.

Streaming quality is now selectable alongside download quality. A cap is
applied by rewriting YouTube's HLS master playlist down to the best
variant at or below the chosen height, keeping its audio group. That
playlist is served from a small loopback HTTP server: Safari's native
HLS is backed by AVFoundation, which cannot read blob: or custom-scheme
URLs, so a Blob URL silently fails to play.

Settings closes with an icon button and its selects match the shared
control height.
2026-08-29 11:28:05 +02:00
vincent 211823f265 feat: custom transport bar, live refresh, collapsible sidebar
Replace WebKit's native video controls with our own. WebKit puts
fullscreen, Picture-in-Picture and volume as overlay buttons in the
video's top corners with no way to move them; owning the bar is the only
way to get every control into one strip along the bottom. Right-click on
the player is suppressed — its menu acted on a video the app does not
control.

One spinner now covers both waits: resolving the stream URL and
buffering it. It no longer sticks after a resume-seek, which fires
'waiting' after 'playing'.

Also: Open on YouTube and a new Download are real buttons in the player;
refresh is a compact icon; Settings is a cog; the subscription sidebar
collapses and reappears on a left-edge hover; the feed auto-refreshes
every 10 minutes while online, skipped while the player is open; the
title-bar strip takes the panel colour, since it sits above panels
rather than the page; and downloaded-only, hide-Shorts and the sidebar
state are remembered between launches.
2026-08-29 11:03:29 +02:00