Vincent Rozenberg 41d638b232 feat: read the subscription list off YouTube, and an app-shaped menu bar panel
Takeout is a snapshot you have to go and fetch. This reads the live list
from youtube.com/feed/channels in a browser already signed in.

It runs JavaScript in the page rather than fetching it here, because
the session cannot be borrowed: Chromium encrypts its cookie store with
a per-app Keychain key, and Arc's cookies read with Chrome's key came
back undecryptable — 1346 of 2196, the session cookies among them. The
list is also lazily loaded, so it has to be scrolled to the end, which
only the page can do. Measured against a hand-scrolled count: 208 both
ways.

The browser is brought to the front first. A background tab has its DOM
discarded, so it reports its address while having nothing on it, which
reads exactly like an empty subscription list.

The page gives handles, not channel ids. Most belong to channels already
known here and are matched by name, costing nothing; only the rest are
looked up, four at a time. That mattered more than it sounds: with the
name selector wrong every name came back empty, nothing matched, all 208
were looked up blind, and the result claimed 58 channels should be
deleted. With names read correctly it is 4 lookups and 0 deletions.
Names arriving doubled — "3D OCD 3D OCD", which reads fine and matches
nothing — are collapsed.

osascript prints a string result in source form, quoted with the
newlines escaped, so a list of rows arrived as one line that looked
like no rows at all. It is unquoted before parsing.

The menu bar item is now a window, not an NSMenu: the app's own type,
spacing and colours, hanging from the icon and closing on blur. Adding a
channel is gone from it — the sidebar already does that.

Replacing the subscription list is confirmed in the main window, never
in a panel that closes when you look away, and the confirmation counts
what would go before anything happens.

Verified end to end: 208 read, 4 looked up, 0 removed, 48 downloads
untouched.
2026-09-04 01:34:29 +02:00

FlightTube

A desktop app that merges all your YouTube subscriptions into one newest-first feed, downloads the videos you pick, and — when you have no connection — shows only what you already downloaded. Built for the flight.

Tauri 2 · Rust · React · Tailwind CSS 4 · SQLite · self-contained (bundles yt-dlp + ffmpeg)

Styled to DESIGN-SYSTEM.md: slate and sky, a 915px type ladder, outline-first controls, borders for separation and shadows only for elevation, with light and dark both designed rather than one derived from the other.

How it works

Subscriptions come from Google Takeout, not the API. Export YouTube subscriptions from Takeout and import the subscriptions.csv in Settings — which carries a seven-step walkthrough with the exact URLs. No OAuth, no API key, no Google Cloud project, nothing secret on disk.

Importing replaces your subscription list: the CSV becomes the whole truth. Channels no longer in it are removed along with their videos and downloaded files. A confirmation dialog names exactly what will be deleted before anything happens.

Video metadata comes from YouTube's public Atom feed — one request per channel to youtube.com/feeds/videos.xml?channel_id=…, fetched 8 at a time and merged into a single feed sorted by publish date. Thumbnails are mirrored to local disk so the feed still renders with no network.

Undownloaded videos stream in-app. YouTube's iframe embed refuses a Tauri window — its origin is tauri://localhost, not an http(s) origin, which produces "Error 153" — so yt-dlp resolves YouTube's HLS master playlist instead. Its variants are H.264 + AAC up to 1080p, which AVFoundation plays natively in WKWebView with adaptive bitrate. One player element serves both local files and streams.

Downloads shell out to yt-dlp, pinned to H.264 video + AAC audio in an MP4 container. That caps quality at 1080p — YouTube only serves H.264 that high, and anything above it is VP9 or AV1, which the app's own player cannot reliably decode. The tradeoff is deliberate: every download is guaranteed to play inside FlightTube.

Requirements

None at runtime — yt-dlp, ffmpeg and ffprobe ship inside the app as Tauri sidecars, so a fresh Mac needs nothing installed. Settings shows each tool's version and whether it came from the bundle or the system; a copy on your machine takes precedence, which is how you run a newer yt-dlp than the bundled one.

The binaries are not in git (~140 MB together). Fetch them once before building:

./scripts/fetch-binaries.sh

That pulls static arm64 ffmpeg/ffprobe from osxexperts.net (Homebrew's ffmpeg links a dozen dylibs and cannot be relocated into a bundle), the yt-dlp zipapp, and a portable CPython from astral's python-build-standalone.

The interpreter is bundled deliberately. The official yt-dlp_macos binary is a PyInstaller one-file build that unpacks 37 MB on every invocation — about eight seconds per call, which every stream resolve would pay. The 3 MB zipapp on a portable interpreter starts in under half a second. macOS ships only Python 3.9 and yt-dlp requires 3.10+, which is why the interpreter has to come too.

Static ffmpeg builds are GPL, which matters if you redistribute.

Running it

npm install && npm run tauri dev

Build and install a real app bundle:

npm run tauri build -- --bundles app && cp -R src-tauri/target/release/bundle/macos/FlightTube.app /Applications/

Using it

  1. Settings — follow the seven-step Takeout guide, then Import subscriptions.csv.
  2. Refresh — pulls the latest videos from every channel.
  3. Download on any video — progress shows live on the button; click again to cancel.
  4. Click any video to play it in the app — a downloaded one from disk, anything else streamed. Nothing hands off to a browser.
  5. List or Tiles — switch layouts in the top bar; the choice is remembered.
  6. Offline — the app detects a lost connection and collapses the feed to your downloads. The connectivity pill also toggles a forced offline mode for testing.

Videos land in ~/Movies/FlightTube (changeable in Settings). Appearance follows the system by default; Settings offers System / Light / Dark.

Limitations

  • The Atom feed returns only the ~15 most recent videos per channel. There is no backfill and no pagination — this is a rolling recent window, not an archive.
  • Quality tops out at 1080p, by the deliberate choice described above.
  • The bundled yt-dlp is frozen at build time and YouTube changes often. Re-run ./scripts/fetch-binaries.sh and rebuild, or install a newer one on your system — the app prefers a system copy when it finds one.
  • yt-dlp warns that extraction without a JavaScript runtime (deno) is deprecated. Format lists are currently identical either way, but that may not hold; installing deno, or a system yt-dlp that depends on it, is the escape hatch.
  • Downloading videos is contrary to YouTube's Terms of Service.

Tests

cd src-tauri && cargo test

42 unit tests cover the three places malformed input actually bites: Takeout CSV parsing, Atom feed parsing (against a captured real response), and yt-dlp progress-line parsing — plus the database rules that a refresh must never clobber download state and that a replacing import drops exactly the right channels.

Two network-dependent tests are excluded by default:

cargo test --test pipeline -- --ignored --nocapture      # full pipeline vs. live feeds
cargo test --test seed_real_db -- --ignored --nocapture  # populate the installed app's db

Layout

src-tauri/src/
  takeout.rs     subscriptions.csv -> channels        (pure, tested)
  feed.rs        Atom XML -> videos, plus fetching    (parse is pure, tested)
  downloader.rs  yt-dlp arguments and progress lines  (pure, tested)
  db.rs          schema and every SQL statement
  thumbs.rs      local thumbnail cache
  net.rs         reachability probe
  commands.rs    Tauri command surface — delegates only
src/
  components/    Sidebar, TopBar, VideoRow, VideoTile, DownloadButton, Player, Settings
  components/ui.tsx        the design system's component vocabulary, in one place
  components/TakeoutGuide  the seven-step export walkthrough
  hooks/         useFeed, useDownloads, useConnectivity, useAppearance

Design notes and the implementation plan are in docs/superpowers/.

S
Description
Offline-first YouTube subscription reader for macOS — Tauri, React, yt-dlp.
Readme
1.7 MiB
Languages
Rust 51.9%
TypeScript 46.5%
CSS 0.8%
Shell 0.6%
HTML 0.2%