Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ if [[ -z "$staged" ]]; then
fi

if ! command -v xcrun >/dev/null 2>&1; then
red "xcrun not available install Xcode command-line tools."
red "xcrun not available; install Xcode command-line tools."
exit 1
fi

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ name: Auto-tag
#
# For non-patch bumps (minor / major), use the manual "Bump & Release"
# workflow instead. For docs-only / dependabot / hooks-only commits, the
# paths filter below means this workflow doesn't even start no tag, no
# paths filter below means this workflow doesn't even start, no tag, no
# release, no inflation.

on:
push:
branches: [main]
paths:
# Only files that affect the produced binary trigger an auto-tag.
# Workflow / CI changes do *not* that was the source of the
# Workflow / CI changes do *not*, that was the source of the
# version-inflation we got while iterating on the workflows themselves.
- "Sources/**"
- "Tests/**"
Expand All @@ -43,7 +43,7 @@ jobs:

- name: Skip if HEAD already has a v* tag
# If the commit is already tagged (e.g. the Bump & Release workflow
# just put one here) there's nothing to do and re-tagging would
# just put one here) there's nothing to do, and re-tagging would
# collide. Sets `skip=true` for downstream conditions.
id: skip
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
# Only fires the actual job when a PR carries the `run-ci` label, or when
# someone manually clicks "Run workflow" keeps macOS minutes from being
# someone manually clicks "Run workflow", keeps macOS minutes from being
# burned on every push.
pull_request:
types: [opened, synchronize, reopened, labeled]
Expand Down
24 changes: 12 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Contributing to YouMenuTube

Thanks for considering a contribution! YouMenuTube is a small SwiftUI
menu-bar app and the codebase is intentionally compact read the README
menu-bar app and the codebase is intentionally compact; read the README
first to get a feel for what the app does and the constraints it operates
under (it's an unofficial client wrapping YouTube's internal **InnerTube**
API; see the disclaimer in the README).
Expand All @@ -21,15 +21,15 @@ Then ⌘R in Xcode. Requires **macOS 15 Sequoia** and **Xcode 16+**.

See the "Project layout" section of the [README](README.md). High-level:

- `Sources/App/` `@main` entry, scenes, root view.
- `Sources/Services/` `YouTubeService` (the only network surface),
- `Sources/App/`: `@main` entry, scenes, root view.
- `Sources/Services/`: `YouTubeService` (the only network surface),
`PlayerController`, `RefreshTrigger`, `Keychain`.
- `Sources/Views/` one SwiftUI view per tab, plus the player and sign-in
- `Sources/Views/`: one SwiftUI view per tab, plus the player and sign-in
windows. `VideoFeedList` / `VideoList` / `ErrorInline` are the shared
building blocks.
- `Sources/Models/` `VideoEntry`, `PlaylistEntry`.
- `Sources/Utilities/` `WindowID`, `UserAgent`, `BuiltInPlaylist`.
- `Tests/` Swift Testing target.
- `Sources/Models/`: `VideoEntry`, `PlaylistEntry`.
- `Sources/Utilities/`: `WindowID`, `UserAgent`, `BuiltInPlaylist`.
- `Tests/`: Swift Testing target.

## Format, lint, test

Expand Down Expand Up @@ -64,7 +64,7 @@ Configuration lives in [`.swift-format`](.swift-format) (line length 120,

## CI

GitHub Actions on every PR but the actual `build-test` job runs **only**
GitHub Actions run on every PR, but the actual `build-test` job runs **only**
when the PR carries the `run-ci` label, or when triggered manually from the
Actions tab. This keeps macOS minutes from being burned on every push. A
maintainer will usually add the label after a quick eyeball.
Expand All @@ -75,11 +75,11 @@ maintainer will usually add the label after a quick eyeball.
- Match the existing style: terse comments, no hypothetical generality, no
dead code, identifiers should carry meaning instead of comments explaining
what code does. (See [`CLAUDE.md` in the system prompt of the maintainers'
AI tooling](.) same principles apply to humans.)
AI tooling](.); same principles apply to humans.)
- Don't add code-level documentation that just narrates the next line. Do
add a short comment when there's a non-obvious *why* (a workaround, a
hidden constraint, a quirk of YouTubeKit / Google fingerprinting, etc.).
- Run `swift-format format -i -r ...` before committing the pre-commit
- Run `swift-format format -i -r ...` before committing; the pre-commit
hook will reject unformatted files.
- Open an issue first for anything that touches the auth flow, the InnerTube
cookie filter, the browser cookie readers, or the player window's WKWebView
Expand All @@ -92,7 +92,7 @@ maintainer will usually add the label after a quick eyeball.

- Imperative mood: "Add Home tab", not "Added" or "Adds".
- Subject ≤ 72 characters; explain *why* in the body, not *what*.
- One topic per commit. We don't squash on merge keep history readable.
- One topic per commit. We don't squash on merge; keep history readable.

## Reporting bugs

Expand All @@ -106,7 +106,7 @@ soon). For now include:
- Relevant log output:
`log stream --predicate 'subsystem == "app.youmenutube"' --level debug`

For **security** issues, see [`SECURITY.md`](SECURITY.md) please don't
For **security** issues, see [`SECURITY.md`](SECURITY.md); please don't
file them publicly.

## License
Expand Down
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

This product bundles the following third-party software:

YouTubeKit (https://github.com/b5i/YouTubeKit) MIT License
YouTubeKit (https://github.com/b5i/YouTubeKit): MIT License
Copyright 2023 - 2025 Antoine Bollengier
52 changes: 26 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Website: <https://youmenutube.riccardo.lol/>

A macOS menu-bar app that puts YouTube in your menubar:

- **Home** (default tab) YouTube's main recommendations feed
- **Home** (default tab): YouTube's main recommendations feed
- Latest videos from your **Subscriptions**
- Browse your **Playlists** (including **Watch Later** and **Liked Videos**)
- **Search** YouTube
Expand All @@ -20,14 +20,14 @@ A macOS menu-bar app that puts YouTube in your menubar:
- In-app **update check** against the latest GitHub release

Built with SwiftUI `MenuBarExtra`, targeting **macOS 15 Sequoia or later**.
Powered by [YouTubeKit](https://github.com/b5i/YouTubeKit) talks to
Powered by [YouTubeKit](https://github.com/b5i/YouTubeKit). It talks to
YouTube's own internal "InnerTube" API directly. **No Google Cloud project,
no API key, no OAuth client to set up.** Sign into youtube.com in your
usual browser passkeys, password managers, all of it and YouMenuTube
usual browser (passkeys, password managers, all of it) and YouMenuTube
imports the session. Supports Safari, Chrome, Edge, Arc, Brave, Vivaldi,
Opera, Helium, Firefox and Zen.

> ## ⚠️ Important read before installing
> ## ⚠️ Important: read before installing
>
> **YouMenuTube is unofficial and not affiliated with, endorsed by, or
> sponsored by YouTube, Google, or Alphabet.** All trademarks belong to
Expand All @@ -38,7 +38,7 @@ Opera, Helium, Firefox and Zen.
> YouTube's internal **InnerTube** API via [YouTubeKit](https://github.com/b5i/YouTubeKit).
> This is the same surface YouTube's own website uses, but it is **not a
> public API** and using it is **arguably outside YouTube's Terms of
> Service**. The API can and occasionally does change or break without
> Service**. The API can (and occasionally does) change or break without
> notice.
>
> **Use at your own risk.** Recommendations:
Expand All @@ -55,7 +55,7 @@ Opera, Helium, Firefox and Zen.
(that link permanently redirects to the newest published version, so you
can bookmark it).
2. Open the DMG and drag `YouMenuTube.app` into `/Applications`.
3. **First launch** because the build is ad-hoc signed (no paid Apple
3. **First launch**: because the build is ad-hoc signed (no paid Apple
Developer ID), Gatekeeper will refuse it on a normal double-click. Either:
- Right-click the app → **Open** → **Open** again in the prompt, *or*
- System Settings → Privacy & Security → "YouMenuTube was blocked …" →
Expand Down Expand Up @@ -129,17 +129,17 @@ YouMenuTube/
## Setup

```sh
brew install xcodegen # (if missingbootstrap.sh will offer to do this for you)
brew install xcodegen # (if missing, bootstrap.sh will offer to install it for you)
./bootstrap.sh
```

This generates `YouMenuTube.xcodeproj`, resolves `YouTubeKit` via SPM, and
opens Xcode. In Xcode:

1. Pick a signing team (target → **Signing & Capabilities → Team**) a free
1. Pick a signing team (target → **Signing & Capabilities → Team**); a free
personal team works.
2. Build & Run (⌘R).
3. There's no Dock icon (by design `LSUIElement`). Look for the ▶️ icon in
3. There's no Dock icon (by design: `LSUIElement`). Look for the ▶️ icon in
the menu bar.
4. Click the icon → **Sign in** → pick the browser where you're already
signed in to youtube.com. First import may ask for a one-time permission
Expand All @@ -153,7 +153,7 @@ opens Xcode. In Xcode:
The project ships a second scheme, **YouMenuTube Demo**, which launches the
app with `-demo-mode`. In that mode `YouTubeService` swaps every network
call for the hardcoded fixtures in
[`Sources/Services/DemoData.swift`](Sources/Services/DemoData.swift)
[`Sources/Services/DemoData.swift`](Sources/Services/DemoData.swift):
popular real video IDs, so thumbnails render normally and clicking a row
plays the real video in the embedded player. No sign-in, no keychain, no
InnerTube traffic. Edit `DemoData.swift` to change what the screenshots
Expand Down Expand Up @@ -184,15 +184,15 @@ YMT_DEMO_MODE=1 ./path/to/YouMenuTube.app/Contents/MacOS/YouMenuTube
YouMenuTube doesn't host a sign-in UI of its own. Instead it imports
`youtube.com` cookies directly out of your browser's cookie store, so the
sign-in itself happens wherever your passkeys / password manager already
work your normal browser. Three formats covered:
work: in your normal browser. Three formats covered:

| Browser family | Storage | What macOS will ask |
|---|---|---|
| **Safari** | Binary cookies inside the Safari container | One-time **Full Disk Access** grant (System Settings → Privacy & Security → Full Disk Access → add YouMenuTube). Without it the import fails cleanly. |
| **Chrome / Edge / Arc / Brave / Vivaldi / Opera / Helium** (Chromium family) | SQLite + AES-128-CBC with a key in the login Keychain | A standard "YouMenuTube wants to use confidential information stored in 'Chrome Safe Storage' …" Keychain prompt. Click **Always Allow** once per browser. (Helium's Keychain entry is `Helium Storage Key` rather than the usual `… Safe Storage`, but the flow is identical.) |
| **Firefox / Zen** (Firefox family) | Plain SQLite, unencrypted | No prompt. |

The importer filters strictly to `*.youtube.com`-scoped rows mixing in
The importer filters strictly to `*.youtube.com`-scoped rows; mixing in
`.google.com` or `accounts.google.com` cookies makes InnerTube respond
with `loggedOut=true`. The resulting blob is persisted in the macOS
Keychain and handed to YouTubeKit via `YouTubeModel.cookies`.
Expand All @@ -204,14 +204,14 @@ once signed in and click **Import**.

The Now Playing window wraps `youtube.com/embed/<id>` in a tiny HTML page
loaded with `baseURL = https://youmenutube.local/`. The fake-but-real-looking
parent origin is what makes YouTube's IFrame player initialize — loading
parent origin is what makes YouTube's IFrame player initialize. Loading
the embed URL top-level returns error 153, and `baseURL = youtube.com`
(same-origin parent) returns 152-4.

The window itself uses `.windowStyle(.plain)` for a fully chrome-less look,
locked to a 16:9 aspect ratio via `NSWindow.aspectRatio`. Because plain
(borderless) windows can't normally become key or be resized, the underlying
`NSWindow`'s styleMask is patched to add `.resizable` which restores both
`NSWindow`'s styleMask is patched to add `.resizable`, which restores both
edge-resize handles and Cmd+W. A 28pt strip at the top of the player fades
in on hover, hosting a small ✕ close button and a `performDrag(with:)`-backed
draggable region (since the WKWebView itself swallows mouse events). By
Expand All @@ -234,19 +234,19 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for PR conventions and detail.

Releases are fully automated. The pipeline:

1. **Auto-tag** (`.github/workflows/auto-tag.yml`) every push to `main` that
1. **Auto-tag** (`.github/workflows/auto-tag.yml`): every push to `main` that
touches `Sources/`, `Tests/`, `project.yml`, or `bootstrap.sh` gets a patch
bump (e.g. `v0.1.5 → v0.1.6`). Docs / dependabot / hook / format-config
pushes don't trigger it.
2. **Release** (`.github/workflows/release.yml`) fires on `v*` tag pushes,
2. **Release** (`.github/workflows/release.yml`): fires on `v*` tag pushes,
builds the `.app` (Release config), ad-hoc signs it, packages it as a
`.dmg` with `create-dmg`, and publishes a GitHub Release.
3. **Bump & Release** (`.github/workflows/bump-release.yml`) manual entry
3. **Bump & Release** (`.github/workflows/bump-release.yml`): manual entry
point (Actions tab → Run workflow) for `minor` / `major` bumps. Patches
are handled by Auto-tag.

Versioning: `CFBundleShortVersionString` is derived from `git describe`
(post-build script in `project.yml`) `0.1.0` on a tag, `0.1.0+N` past it.
(post-build script in `project.yml`): `0.1.0` on a tag, `0.1.0+N` past it.
`CFBundleVersion` is `git rev-list --count HEAD` (monotonic, kept for
macOS update bookkeeping even though it isn't shown in UI). `GitCommit`
holds the short SHA (with a `-dirty` suffix when the working tree has
Expand All @@ -255,28 +255,28 @@ commit, e.g. `0.1.0 · 19d5410`.

## Troubleshooting

- **Import says "Safari's cookies live inside a protected container"**
- **Import says "Safari's cookies live inside a protected container"**:
macOS requires **Full Disk Access** to read `~/Library/Containers/com.apple.Safari/…`.
System Settings → Privacy & Security → Full Disk Access → add YouMenuTube
(the Import window has a shortcut button for this). Re-run Import.
- **Import says "Couldn't read [Browser]'s cookie-encryption key from the
Keychain"** you clicked Deny on the macOS Keychain prompt. Open
Keychain"**: you clicked Deny on the macOS Keychain prompt. Open
Keychain Access, search for "Safe Storage" for that browser, open the
entry → **Access Control** tab → remove YouMenuTube from the deny list
(or delete the ACL), then re-run Import and click **Always Allow**.
- **Import says "[Browser] isn't signed in to YouTube"** the browser
- **Import says "[Browser] isn't signed in to YouTube"**: the browser
you picked doesn't have a valid YouTube session. Click the
"Sign in to YouTube in [Browser]" button, complete sign-in there, then
re-run Import.
- **Subscriptions / playlists empty after signing in** enable verbose
- **Subscriptions / playlists empty after signing in**: enable verbose
logging to see what the import produced:
`log stream --predicate 'subsystem == "app.youmenutube"' --level debug`.
If session markers (SAPISID, SID, LOGIN_INFO, __Secure-3PSIDTS) are all
present but InnerTube still says `loggedOut=true`, YouTubeKit may be out
of date check for a newer release.
- **Embed player shows error 150 / 101** the uploader has disabled
of date; check for a newer release.
- **Embed player shows error 150 / 101**: the uploader has disabled
embedding for that specific video; it can only be played on youtube.com.
- **Embed player shows error 153 / 152-4** should not happen with the
- **Embed player shows error 153 / 152-4**: should not happen with the
current wrapper; if it does, check `Sources/Views/PlayerWindow.swift` for
changes to `baseURL` or the iframe `origin` query parameter.

Expand All @@ -290,7 +290,7 @@ network only for:
- `api.github.com/repos/gek0z/YouMenuTube/releases/latest` once per launch
to render the update-available link in Settings → About

Sign-in itself happens in your browser, not in YouMenuTube the app
Sign-in itself happens in your browser, not in YouMenuTube; the app
doesn't make network requests to Google / YouTube for authentication. It
reads `youtube.com` cookies out of your browser's on-disk cookie store
(filtered to `*.youtube.com` only), stores them in the macOS Keychain
Expand Down
12 changes: 6 additions & 6 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Please **do not** open a public GitHub issue for security problems.
Use one of the following private channels:

- GitHub's [private vulnerability reporting](https://github.com/gek0z/YouMenuTube/security/advisories/new)
(preferred gives us a private discussion thread + draft advisory).
(preferred: gives us a private discussion thread + draft advisory).
- Email the maintainer (see the GitHub profile of [@gek0z](https://github.com/gek0z)).

Please include:
Expand All @@ -25,12 +25,12 @@ Please include:
4. Whether you'd like to be credited in the advisory.

We aim to acknowledge reports within 7 days. Because this is a hobby project,
fix turnaround depends on availability critical issues will be prioritised.
fix turnaround depends on availability; critical issues will be prioritised.

## Supported versions

Only `main` is supported. Older tagged releases will not receive backported
fixes upgrade to the latest release.
fixes; upgrade to the latest release.

## Scope

Expand All @@ -43,7 +43,7 @@ In scope:
Out of scope:

- Vulnerabilities in YouTube, Google, or any first-party Apple framework.
- Vulnerabilities in third-party dependencies (report those upstream currently
- Vulnerabilities in third-party dependencies (report those upstream; currently
only [YouTubeKit](https://github.com/b5i/YouTubeKit)).
- The fact that YouMenuTube uses YouTube's internal **InnerTube** API. This is
documented in the README and is a deliberate design choice, not a security
Expand All @@ -67,7 +67,7 @@ of the selected browser's on-disk cookie store:
- **Safari** requires the user to grant YouMenuTube **Full Disk Access**
(System Settings → Privacy & Security → Full Disk Access) because the
Safari cookie file lives inside a protected container. Without this grant
the reader fails cleanly it cannot silently read Safari cookies.
the reader fails cleanly; it cannot silently read Safari cookies.
- **Chromium-family browsers** (Chrome, Edge, Arc, Brave, Vivaldi, Opera,
Helium) encrypt their cookie values with an AES key stored in the macOS
login Keychain under e.g. `Chrome Safe Storage` (or `Helium Storage Key`
Expand All @@ -89,4 +89,4 @@ present. No other domain's cookies are ever persisted or sent.
InnerTube endpoints.
- No background activity when the menu bar popover is closed.

If you find evidence to the contrary, that's a bug please report it.
If you find evidence to the contrary, that's a bug; please report it.
2 changes: 1 addition & 1 deletion Sources/Services/BrowserCookieImport/Browser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ enum Browser: String, CaseIterable, Identifiable {
/// as a `kSecClassGenericPassword` entry in the login keychain.
///
/// Most Chromium forks use "<Brand> Safe Storage" with account "<Brand>",
/// but Helium uses "Helium Storage Key" / "Helium" so this is handled
/// but Helium uses "Helium Storage Key" / "Helium", so this is handled
/// per-case rather than synthesised from `displayName`.
var chromiumSafeStorageService: String? {
switch self {
Expand Down
Loading