diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 064cb33..aed0b5d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,7 +39,7 @@ jobs: if: runner.os == 'Windows' run: choco install ffmpeg --no-progress -y - name: Install test tools - run: python -m pip install pytest "yt-dlp[default]==2026.8.19" + run: python -m pip install --upgrade pytest "yt-dlp[default]" - name: Verify executables run: | ffmpeg -version diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f63eef..654766d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All notable changes to `/watch` are documented here. ### Fixed - SessionStart hook command now quotes `${CLAUDE_PLUGIN_ROOT}`, so plugin paths containing spaces work. - Uniform sampling now spans the actual capped range and labels selected source frames with their real timestamps, including fractional seeks and cue frames. -- FFmpeg sync-option capability detection, empty keyframe-window fallback, RGB thumbnail deduplication, and actionable probe/launch errors. +- FFmpeg sync-option capability detection (including FFmpeg 9's `-fps_mode[:]` help format; tested on FFmpeg 4.4 through 9.0), empty keyframe-window fallback, RGB thumbnail deduplication, and actionable probe/launch errors. - UTF-8 output and subprocess decoding, BOM-aware shared config parsing, provider-key pairing, atomic config updates, keyless readiness, and advisory setup hooks. - Bounded caption downloads respect watch-owned output templates while retaining intentional yt-dlp configuration. Fresh run directories and completed final-path validation prevent stale/partial media reuse. - WebVTT optional-hour timestamps, entities, cue/block parsing, and overlap-scoped deduplication preserve independent repeated speech. @@ -23,6 +23,7 @@ All notable changes to `/watch` are documented here. - Conservative 24,000,000-byte cloud upload budgets, actual chunk/multipart size checks, and explicit partial/no-speech metadata. ### Changed +- Only the latest yt-dlp release is supported. An HTTP 403 now tells the agent to update yt-dlp with its owning package manager and retry once; CI tests against the latest release instead of a pinned one. - First-success installation documentation separates media tools from optional speech backends and describes host/network limitations. - User-provided output directories now contain a disposable run child; cleanup preserves the parent and source files. - The base runtime remains standard-library-only; local model environments and caches live outside the self-contained skill folder. diff --git a/README.md b/README.md index faff4b7..7c06fec 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Tell me which tools are missing and help me install them here. | Ubuntu/Debian | `sudo apt install python3 ffmpeg pipx`, then `pipx install "yt-dlp[default,curl-cffi]"` and `pipx ensurepath`. Install [Deno](https://docs.deno.com/runtime/getting_started/installation/) for YouTube. | | Windows | Install [Python 3.10+](https://www.python.org/downloads/windows/), then `winget install --id Gyan.FFmpeg --exact`, `winget install --id yt-dlp.yt-dlp --exact`, and `winget install --id DenoLand.Deno --exact`. | -Reopen the terminal and agent after installation so they can find the new tools. On Windows, verify Python with `python --version` or `py -3 --version`. The tested yt-dlp baseline is **2026.08.19**; Watch uses the executable available in the active environment. +Reopen the terminal and agent after installation so they can find the new tools. On Windows, verify Python with `python --version` or `py -3 --version`. Watch supports the **latest yt-dlp release** only, because sites routinely break older ones; it uses the executable available in the active environment. ## Using a web app? @@ -311,7 +311,7 @@ Update Watch using the same method you installed it with, then start a new sessi Marketplace auto-update behavior depends on the host and your settings. Updating Watch is separate from updating its media tools. -Update yt-dlp with its owning installer, then verify the same executable with `yt-dlp --version`: +Keep yt-dlp on its latest release. Update it with its owning installer, then verify the same executable with `yt-dlp --version`: - Homebrew: `brew upgrade yt-dlp` - pipx: `pipx upgrade yt-dlp` @@ -331,7 +331,7 @@ Ask the agent to run bundled `setup.py --json` for resolved paths/versions, JS-r | Python opens the Store | Use an installed interpreter verified by `python --version` or `py -3 --version`. | | FFmpeg option failure | Inspect the actual FFmpeg path; watch probes `-fps_mode` and retains advertised `-vsync` compatibility for older builds. | | Missing JS runtime/EJS | Update the owning yt-dlp package and follow upstream Deno/EJS setup. | -| 403 / login challenge | Read the original error; use explicit authentication only if you have access. A 403 has no universal workaround. | +| 403 / login challenge | Update yt-dlp to its latest release (see [Updating and troubleshooting](#updating-and-troubleshooting)) and retry once; the agent does this automatically. If the 403 persists, read the original error and use explicit authentication only if you have access. | | `Gemini auth` / `quota` / `rejected` / `upload` / `service` / `network` / `response` | The Gemini engine failed: bad or missing key, rate limit, a video Google refused (private, unsupported, too long), a failed upload, a Google-side error, no route to `generativelanguage.googleapis.com`, or an unreadable reply. Nothing ran locally; fix the cause or rerun with `--engine local`. | | 429 | Wait before retrying; the service is rate limiting requests. | | Explicit hosted egress denial | Check the environment's network settings or use an accessible local source. Cloud ASR/cold model setup still need network access. | diff --git a/skills/watch/SKILL.md b/skills/watch/SKILL.md index 30d2b49..e6ab3ae 100644 --- a/skills/watch/SKILL.md +++ b/skills/watch/SKILL.md @@ -150,7 +150,7 @@ If a small-model transcript is nonsense, suggest the actual spoken-language hint Cloud fallbacks extract mono 16 kHz MP3 and upload within a conservative 24,000,000-byte file budget. Large files are chunked with source-time offsets restored; missing chunks appear in the final report. Provider errors do not justify automatic provider switching. -For download failures, use the bounded original error and its diagnostic hint. A generic 403 has no universal fix. Do not hardcode alternate clients, cycle cookies, or disable TLS verification. Preserve available captions when media/probing fails. For hosted environments, local uploads solve downloading only; cloud ASR and cold local-model setup still need permitted network access. +For download failures, use the bounded original error and its diagnostic hint. A 403 has no generic fix, but first update yt-dlp with its owning package manager (e.g. `brew upgrade yt-dlp`, `pipx upgrade yt-dlp`) and retry once. Do not hardcode alternate clients, cycle cookies, or disable TLS verification. Preserve available captions when media/probing fails. For hosted environments, local uploads solve downloading only; cloud ASR and cold local-model setup still need permitted network access. For follow-ups, reuse evidence already viewed before rerunning. Remove only the disposable **Work dir** created by this invocation when no longer needed. Never delete the parent supplied with `--out-dir`, a user source file, the local venv, or model caches as routine cleanup. diff --git a/skills/watch/scripts/download.py b/skills/watch/scripts/download.py index 8bf3b9e..158958a 100755 --- a/skills/watch/scripts/download.py +++ b/skills/watch/scripts/download.py @@ -54,6 +54,9 @@ def network_diagnostic(stderr: str) -> str: hint = 'This source requires authentication; supply an explicit cookie option if you have access.' elif '429' in lower or 'too many requests' in lower: hint = 'The service is rate limiting requests; wait before trying again.' + elif re.search(r'\b403\b', lower) or 'forbidden' in lower: + # Only the latest yt-dlp is supported; sites routinely break older releases with 403s. + hint = 'No generic fix for a 403; update yt-dlp to its latest release and retry once.' elif any(s in lower for s in ('egress denied', 'not in the allowlist', 'blocked by network policy')): hint = 'This environment blocks the destination; check its network settings or use an accessible local file.' elif any(s in lower for s in ('certificate_verify_failed', 'certificate verify failed', 'unable to get local issuer certificate')): diff --git a/tests/test_download.py b/tests/test_download.py index a800f82..ea77391 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -100,7 +100,7 @@ def test_rejects_outside_final_path(monkeypatch, tmp_path): @pytest.mark.parametrize('message,hint', [ - ('HTTP 403 forbidden', 'original error'), ('HTTP 429', 'rate limiting'), + ('HTTP Error 403: Forbidden', 'latest release'), ('unexpected extractor failure', 'original error'), ('HTTP 429', 'rate limiting'), ('Sign in to continue', 'authentication'), ('No JavaScript runtime', 'Deno/EJS'), ('egress denied', 'network settings'), ('certificate verify failed', 'trusted CA'), ])