Replaces the official yt-dlp_macos sidecar. That build is PyInstaller one-file and unpacks 37MB on every invocation, costing ~8s per call on this machine — ruled out signing, xattrs and thinning the universal binary as causes, and it is 8% CPU over 8s, so it is the unpack itself. Every stream resolve paid it. The 3MB zipapp on a trimmed portable CPython starts in ~0.45s, a 17x improvement; a real stream resolve goes from ~10s to ~2.4s. macOS ships only Python 3.9 and yt-dlp needs 3.10+, hence bundling an interpreter. yt-dlp is now resolved as an argv prefix rather than a path, so a system install (a plain script, instant) still takes precedence. Bundle grows 153MB -> 219MB, which buys back the responsiveness.
141 lines
6.4 KiB
Markdown
141 lines
6.4 KiB
Markdown
# 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 9–15px 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](https://takeout.google.com/) 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:
|
||
|
||
```bash
|
||
./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
|
||
|
||
```bash
|
||
npm install && npm run tauri dev
|
||
```
|
||
|
||
Build and install a real app bundle:
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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/`.
|