Send any YouTube or YouTube Music video to the Flow desktop app to watch or download — one click, no accounts, no scraping. The extension only ever transmits a video id (plus optional timestamp / playlist id); all extraction and playback happen natively inside Flow.
From a release (recommended)
- Download the latest build from Releases:
flow-extension-chromium-vX.Y.Z.zipfor Chrome / Edge / Brave / Opera / Vivaldiflow-extension-firefox-vX.Y.Z.zipfor Firefox
- Unzip it.
- Load it:
- Chromium —
chrome://extensions→ enable Developer mode → Load unpacked → select the unzipped folder. - Firefox —
about:debugging→ This Firefox → Load Temporary Add-on → pickmanifest.jsoninside the unzipped folder.
- Chromium —
You also need the Flow desktop app installed for the handoff to do anything.
Every action runs the same handoff cascade:
- Silent bridge — if Flow is running, an HTTP request to
127.0.0.1hands off instantly with no prompt. - Deep link — otherwise a
flow://link launches Flow if it's installed. - Get Flow — if nothing handles it, a toast links to the desktop release.
Surfaces: a button row under the watch-page player, a hover overlay on any video thumbnail, and right-click context-menu items.
src/
lib/ constants, browser shim, settings, permissions, query
bridge/ ping · http · deep-link · handoff cascade
extract/ videoId / timestamp / playlist / selectors
ui/ shadow host · styles · button · watch-row · overlay · toast
content/ params · actions · navigation · messaging · main (entry)
background/ context-menus · service-worker (entry)
popup/ toolbar status + quick toggles
options/ full settings
manifest/ base + per-browser manifests
scripts/ icon rasteriser · zip · package · version check
build.mjs esbuild bundler → dist/chromium + dist/firefox
Each module does one thing; nothing imports YouTube or Flow CSS. All injected UI lives in a Shadow DOM.
npm install
npm run build # → dist/chromium and dist/firefox
npm run dev # rebuild on change (unminified, with logging)
npm run package # zip both builds into release/
npm run release # build + package
npm run format # format with PrettierIcons are generated from the Flow logo by a dependency-free SVG rasteriser (scripts/), so no binary assets are committed.
Silent mode calls 127.0.0.1, which needs an optional host permission requested on demand (from the popup or Settings). Without it the extension still works via the flow:// deep link — just with the browser's "Open Flow?" prompt. The popup shows live connection status once access is granted.
Releases are automated. To cut vX.Y.Z:
- Bump
versioninpackage.json. git tag vX.Y.Z && git push --tags.
The release workflow verifies the tag matches package.json, builds and packages both browsers, and publishes the zips to GitHub Releases with generated notes.
Shared with Flow Desktop — keep both sides identical.
flow://watch?v=<ID>[&t=<SECONDS>][&list=<PLAYLIST_ID>]
flow://download?v=<ID>
flow://music?v=<ID>
GET http://127.0.0.1:47893/flow/ping → { "app": "flow", "version": "…" }
GET http://127.0.0.1:47893/flow/watch ?v,t?,list?
GET http://127.0.0.1:47893/flow/download ?v
- Ports probed in order:
47893,47894,47895(configurable in Settings). - Video id:
^[A-Za-z0-9_-]{11}$. Playlist id:^[A-Za-z0-9_-]{10,64}$. - The desktop side validates every field and echoes CORS only for the allow-listed YouTube origins.