From 03f1abc9aee4927b1eb685ac2c04b6f9fea43070 Mon Sep 17 00:00:00 2001 From: Abdur Rahman Date: Wed, 13 May 2026 13:19:41 -0400 Subject: [PATCH] Add --cookies-from-browser flag for login-walled sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit yt-dlp can read existing browser session cookies via --cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]. Surface that as a /watch flag so Instagram Reels, private YouTube videos, and some X posts work without a manual yt-dlp invocation. Off by default — public sources are unchanged. - watch.py: new --cookies-from-browser BROWSER argparse flag, passed through to download(). - download.py: download_url() / download() accept cookies_from_browser kwarg and inject "--cookies-from-browser " into the yt-dlp argv. Rejects values starting with "-" to defend against accidental flag injection. CLI entry point accepts an optional 3rd positional arg for parity. - SKILL.md: documents the new flag, updates the "Download fails" recipe to point at it as the documented retry path for login-required sources, and discloses in the Security & Permissions section that cookies are read transiently by yt-dlp — never written, logged, or sent anywhere except the URL's host. - CHANGELOG.md: [Unreleased] entry. Tested end-to-end on https://www.instagram.com/reel/DX9k8WDPZ2D/ — IG blocked the default download path with "login required", retry with --cookies-from-browser chrome succeeded and produced an mp4 + audio that the rest of the watch pipeline consumed normally. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 5 +++++ SKILL.md | 6 ++++-- scripts/download.py | 22 +++++++++++++++------- scripts/watch.py | 15 ++++++++++++++- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad03c4..d1aeed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `/watch` are documented here. +## [Unreleased] + +### Added +- `--cookies-from-browser BROWSER` flag on `watch.py` (forwarded to yt-dlp) for sources behind a login wall — Instagram Reels, private YouTube videos, some X posts. Accepts yt-dlp's full grammar (`chrome`, `firefox:default`, `safari`, etc.). yt-dlp reads the user's existing browser session transiently; no cookies are written to disk, logged, or sent anywhere except the URL's host. Off by default. `SKILL.md` updated with usage notes and a security disclosure; `Failure modes` now points at the flag as the documented retry path for login-required downloads. + ## [0.1.3] — 2026-05-09 ### Fixed diff --git a/SKILL.md b/SKILL.md index acd057d..cb599bb 100644 --- a/SKILL.md +++ b/SKILL.md @@ -83,6 +83,7 @@ Optional flags: - `--out-dir DIR` — keep working files somewhere specific (default: an auto-generated tmp dir) - `--whisper groq|openai` — force a specific Whisper backend (default: prefer Groq if both keys exist) - `--no-whisper` — disable the Whisper fallback entirely (frames-only if no captions) +- `--cookies-from-browser BROWSER` — forward to yt-dlp. Use when a source is login-walled (Instagram Reels, private YouTube videos, some X posts). Accepts yt-dlp's full grammar: `BROWSER[+KEYRING][:PROFILE][::CONTAINER]`, e.g. `chrome`, `firefox:default`, `safari`. yt-dlp reads the existing browser session — see "Security & Permissions" below. ### Focusing on a section (higher frame rate) @@ -139,7 +140,7 @@ Both keys live in `~/.config/watch/.env`. The script prefers Groq when both are - **Setup preflight failed** → run `python3 "${CLAUDE_SKILL_DIR}/scripts/setup.py"` (auto-installs ffmpeg/yt-dlp via brew on macOS, scaffolds the `.env`). For API key, ask the user via `AskUserQuestion` and write it to `~/.config/watch/.env`. - **No transcript available** → captions missing AND (no Whisper key OR Whisper API failed). Script prints a hint pointing to setup. Proceed frames-only and tell the user. - **Long video warning printed** → acknowledge it in your answer. Offer to re-run focused on a specific section via `--start`/`--end` rather than a sparse full-video scan. -- **Download fails** → yt-dlp's error goes to stderr. If it's a login-required or region-locked video, tell the user plainly; do not keep retrying. +- **Download fails** → yt-dlp's error goes to stderr. If it's a region-locked video, tell the user plainly; do not keep retrying. If it's login-required (Instagram Reels, private YouTube, some X posts), retry once with `--cookies-from-browser ` to reuse the user's existing browser session — ask which browser they're signed in to first; do not assume. - **Whisper request fails** → the error is printed to stderr (likely: invalid key, rate limit, or 25 MB upload limit on a very long video). The report will say "none available" for transcript. You can retry with `--whisper openai` if Groq failed (or vice versa). ## Token efficiency @@ -155,6 +156,7 @@ If you already watched a video this session and the user asks a follow-up, do ** **What this skill does:** - Runs `yt-dlp` locally to download the video and pull native captions when the source supports them (public data; the request goes directly to whatever host the URL points at) +- When `--cookies-from-browser BROWSER` is passed, yt-dlp reads the user's existing session cookies for that browser and sends them along with the download request to the source platform. Cookies are read transiently by yt-dlp itself — they are never written to the working directory, never logged, never sent anywhere except the platform the URL points at. The user remains signed in to the platform afterwards; nothing about the browser session is changed. - Runs `ffmpeg` / `ffprobe` locally to extract frames as JPEGs and, when Whisper is needed, a mono 16 kHz audio clip - Sends the extracted audio clip to Groq's Whisper API (`api.groq.com/openai/v1/audio/transcriptions`) when `GROQ_API_KEY` is set (preferred — cheaper, faster) - Sends the extracted audio clip to OpenAI's audio transcription API (`api.openai.com/v1/audio/transcriptions`) when `OPENAI_API_KEY` is set and Groq is not, or when `--whisper openai` is forced @@ -163,7 +165,7 @@ If you already watched a video this session and the user asks a follow-up, do ** **What this skill does NOT do:** - Does not upload the video itself to any API — only the extracted audio goes out, and only when native captions are missing AND Whisper is not disabled with `--no-whisper` -- Does not access any platform account (no login, no session cookies, no posting) +- Does not access any platform account on its own — login session cookies are only used when `--cookies-from-browser` is explicitly passed by the user, and even then yt-dlp only sends them to the URL's host. The skill never posts, comments, follows, or modifies the user's account. - Does not share API keys between providers (Groq key only goes to `api.groq.com`, OpenAI key only goes to `api.openai.com`) - Does not log, cache, or write API keys to stdout, stderr, or output files - Does not persist anything outside the working directory and `~/.config/watch/.env` — clean up the working directory when you're done (Step 5) diff --git a/scripts/download.py b/scripts/download.py index afa1ef4..289cdce 100755 --- a/scripts/download.py +++ b/scripts/download.py @@ -59,7 +59,7 @@ def _pick_video(out_dir: Path) -> Path | None: return None -def download_url(url: str, out_dir: Path) -> dict: +def download_url(url: str, out_dir: Path, cookies_from_browser: str | None = None) -> dict: if shutil.which("yt-dlp") is None: raise SystemExit("yt-dlp is not installed. Install with: brew install yt-dlp") @@ -80,9 +80,16 @@ def download_url(url: str, out_dir: Path) -> dict: "--no-playlist", "--ignore-errors", "-o", output_template, - "--", - url, ] + if cookies_from_browser: + # yt-dlp accepts BROWSER[+KEYRING][:PROFILE][::CONTAINER]; reject anything + # that looks like it could be another flag rather than a browser name. + if cookies_from_browser.startswith("-"): + raise SystemExit( + f"--cookies-from-browser value must be a browser name, got {cookies_from_browser!r}" + ) + cmd.extend(["--cookies-from-browser", cookies_from_browser]) + cmd.extend(["--", url]) # yt-dlp may exit non-zero if a subtitle variant fails (e.g. 429) even when # the video itself downloaded fine. Treat "video file present" as success. @@ -117,15 +124,16 @@ def download_url(url: str, out_dir: Path) -> dict: } -def download(source: str, out_dir: Path) -> dict: +def download(source: str, out_dir: Path, cookies_from_browser: str | None = None) -> dict: if is_url(source): - return download_url(source, out_dir) + return download_url(source, out_dir, cookies_from_browser=cookies_from_browser) return resolve_local(source) if __name__ == "__main__": if len(sys.argv) < 3: - print("usage: download.py ", file=sys.stderr) + print("usage: download.py []", file=sys.stderr) raise SystemExit(2) - result = download(sys.argv[1], Path(sys.argv[2])) + cookies = sys.argv[3] if len(sys.argv) > 3 else None + result = download(sys.argv[1], Path(sys.argv[2]), cookies_from_browser=cookies) print(json.dumps(result, indent=2)) diff --git a/scripts/watch.py b/scripts/watch.py index 8aa0600..8fb26d3 100755 --- a/scripts/watch.py +++ b/scripts/watch.py @@ -44,6 +44,19 @@ def main() -> int: default=None, help="Force a specific Whisper backend. Default: prefer Groq, fall back to OpenAI.", ) + ap.add_argument( + "--cookies-from-browser", + type=str, + default=None, + metavar="BROWSER", + help=( + "Forwarded to yt-dlp. Use when a source is login-walled (Instagram, " + "private YouTube, some Twitter/X posts). Accepts the same value as " + "yt-dlp: BROWSER[+KEYRING][:PROFILE][::CONTAINER], e.g. 'chrome', " + "'firefox:default', 'safari'. yt-dlp will read your existing browser " + "session cookies — see SKILL.md for security notes." + ), + ) args = ap.parse_args() max_frames = min(args.max_frames, 100) @@ -59,7 +72,7 @@ def main() -> int: "[watch] downloading via yt-dlp…" if is_url(args.source) else "[watch] using local file…", file=sys.stderr, ) - dl = download(args.source, work / "download") + dl = download(args.source, work / "download", cookies_from_browser=args.cookies_from_browser) video_path = dl["video_path"] meta = get_metadata(video_path)