A stop button appears beside Download all whenever anything is
downloading or waiting to, and disappears when nothing is. It kills the
running processes, marks everything the database still calls queued or
running as cancelled, and clears the part files. Downloads already
finished are untouched.
That also covers two cases a per-video Cancel cannot reach: a download
parked on a slot, which stands down when its turn comes, and a row left
"running" by a crash that no process backs any more.
Cancelling no longer reports itself as a failure. download_video
returned Err("Download was cancelled.") when it found its child gone,
which the caller turned into an error dialog — pressing Stop all would
have raised one per download. Stopping something is a normal outcome:
the state is already recorded and the event already sent, so it returns
cleanly.
A video's channel name under the thumbnail is now a link — click it in
either list or tile view and the feed narrows to that channel. It
clears the search box on the way, so arriving at a channel shows the
channel rather than whatever you had been searching for.
On a channel page, an icon button to the right of Local queues every
video listed there. The backend already runs two downloads at a time
and parks the rest, so the whole channel goes into the queue at once
and comes down in order. The button is only there when there is
something left to fetch, and a video that fails reports it on its own
row rather than raising a dialog per failure.
Cancelling a queued download now actually cancels it. Before, a video
waiting for a slot ignored the cancel and started anyway once its turn
came — barely reachable with one-at-a-time downloading, unmissable
when a whole channel is queued. A download re-reads its own state
after claiming a slot and stands down if it is no longer wanted.
Failing channels are visible instead of buried in a toast count. Each
refresh records its outcome per channel, the sidebar marks the failures
and counts them in its heading, and selecting one explains why above its
videos. Yours turn out to be three channels returning HTTP 404 — removed
or renamed on YouTube.
Video lengths now fill for what is on screen. Filling the newest across
all subscriptions meant a channel's videos stayed blank forever, since
the global newest always won the queue.
yt-dlp can be updated from Settings, which also says whether it is
current. It breaks whenever YouTube changes something, so it lands in
app data and takes precedence over the bundled copy; ffmpeg is stable
and ships with each release, so it is shown but not updated.
Also: 'Downloaded only' is now 'Local'; Hide Shorts moved to Settings;
the seven-step Takeout guide moved behind a button, since it dominated
the panel; the player names the height it is actually streaming, which
changes as an adaptive stream switches rendition; the player's delete is
an icon; and the window minimum drops to 1080 now that the control row
has one fewer button, while still never wrapping.
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.
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.
Window chrome: titleBarStyle Overlay (Transparent left the native bar in
place, white in dark mode, with a dead strip beneath it). The app now
paints that strip itself, so it matches the page background.
Player: prev/next through the feed, a full-screen button, a spinner
while a stream resolves, Open on YouTube on downloaded videos too, and
the description collapsed behind a disclosure.
Downloads default to Best, which reaches real 4K — above 1080p YouTube
serves VP9/AV1, verified to play natively in WKWebView here. Audio stays
pinned to AAC because Opus in MP4 would be silent. A Compatible setting
keeps the old 1080p H.264 behaviour. Files are now named
'<ISO date> - <title> [<id>].mp4'.
Watch progress is recorded and drawn under thumbnails like YouTube's,
and reopening a video resumes where it left off.
Tiles clamp every text line to a fixed height so they share a baseline,
and the search field no longer clips its placeholder.
Fixes a Picture-in-Picture leak: WebKit kept a detached video playing
after the player closed, so a second video could play over the first
with no way to stop it. Every exit path now tears the element down.
Adds a seven-step in-app guide to exporting subscriptions from Google
Takeout, with the real URLs opened in the system browser.
Restyles the app onto the supplied design system: slate/sky palette,
9-15px type ladder, outline-first controls, tiered radii, borders for
separation and shadows only for elevation. Light and dark are both
designed, with a System/Light/Dark control and a pre-paint script so
the window does not flash light on a dark machine.
Importing now replaces the subscription list rather than merging, per
request. Because that can delete downloaded files, a confirmation
dialog names exactly what will go first.
Adds an end-to-end integration test that runs the real pipeline against
live YouTube Atom feeds, verifying the merged feed is newest-first
across channels.
37 unit tests covering the three parsers and the database, including
that a feed refresh preserves download state and that a truncated
feed response fails rather than storing partial results.