yt-tools-cli 0.23.2__tar.gz → 0.24.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/.claude-plugin/plugin.json +1 -1
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/CHANGELOG.md +36 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/PKG-INFO +2 -2
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/README.md +1 -1
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/pyproject.toml +1 -1
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/ensure-install.ps1 +33 -4
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/ensure-install.sh +30 -3
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/skills/using-yt-tools/SKILL.md +2 -2
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_doctor.py +52 -2
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_packaging.py +72 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/doctor.py +25 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/extras.py +11 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/.github/workflows/ci.yml +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/.github/workflows/release.yml +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/.gitignore +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/CONTRIBUTING.md +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/LICENSE +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/hooks/hooks.json +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/check-core-install.sh +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/check-metadata.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/check-pypi-name.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/ensure-install.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/publish-check.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/scripts/sync-version.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/__init__.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_cache.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_check_metadata.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_cli_smoke.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_comments.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_core.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_distribution_name.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_failure_modes.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_frames.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_listen.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_markdown.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_meta.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_missing_extra.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_ocr.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_parse_interval.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_publish_check.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_pypi_name.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_search.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_skill_portable.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_transcript.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_version.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/tests/test_watch.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/__init__.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/_metadata.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/cache.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/cli.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/comments.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/core.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/frames.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/listen.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/markdown.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/meta.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/ocr.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/search.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/transcript.py +0 -0
- {yt_tools_cli-0.23.2 → yt_tools_cli-0.24.1}/yt_tools/watch.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-tools",
|
|
3
3
|
"displayName": "yt-tools",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.24.1",
|
|
5
5
|
"description": "Iterative YouTube watching for agents: clean-markdown transcripts with [mm:ss] anchors, targeted frame extraction, FFT audio analysis (BPM, key, chord progression, spectral features), full-metadata extraction (description, chapters, most-replayed, counts, tags), comment scraping, and OCR over cached frames (RapidOCR PP-OCRv5) for silent-with-text videos. Light core (transcript / metadata / comments / search) with heavy stacks in [frames] and [audio] extras. Bundles the using-yt-tools skill and a SessionStart hook that auto-installs from the plugin's local clone via pipx (default [full] extras, fallback to [frames,audio]) and probes ffmpeg.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Victor Kuznetsov",
|
|
@@ -11,6 +11,42 @@ Releases before 0.8.0 are summarized only in the git log.
|
|
|
11
11
|
|
|
12
12
|
_(nothing yet)_
|
|
13
13
|
|
|
14
|
+
## [0.24.1] — 2026-09-13
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- The plugin install line named a marketplace that no longer exists:
|
|
19
|
+
`yt-tools@opeitcloc03-claude-plugins` → `yt-tools@kzntsv-dev-claude-plugins`.
|
|
20
|
+
The marketplace *name* is a string in `.claude-plugin/marketplace.json`, not a
|
|
21
|
+
property of the GitHub account — renaming the account rewrites paths (and
|
|
22
|
+
redirects the old one), never file contents. The catalog was renamed today;
|
|
23
|
+
this repository's README and skill still pointed at the retired name.
|
|
24
|
+
|
|
25
|
+
## [0.24.0] — 2026-09-13
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `yt-tools doctor` now names the `bpm-detector` enrichment: a `warn` (never a
|
|
30
|
+
blocker) with the exact `pipx inject` command. It is not an extra and cannot be
|
|
31
|
+
one, so nothing else in the report was ever going to mention it — a PyPI
|
|
32
|
+
install had no way to learn the enrichment existed at all.
|
|
33
|
+
- The plugin hook tops the enrichment up on the "version already matches" path.
|
|
34
|
+
Both hooks ask the CLI (`doctor`) and inject only what it reports missing, so
|
|
35
|
+
the hook and the report cannot drift into disagreeing about the environment.
|
|
36
|
+
- A syntax guard for the hook scripts: `bash -n` for the POSIX hook, the
|
|
37
|
+
PowerShell parser for the Windows one. The content guards assert strings, not
|
|
38
|
+
grammar — a stray `fi` left behind by a block restructure passed all of them
|
|
39
|
+
and was caught only by running the hook by hand.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Moving `bpm-detector` out of `[full]` (0.23.1) left it guaranteed by nothing:
|
|
44
|
+
the hook injected it only right after a reinstall, so a machine whose VCS fetch
|
|
45
|
+
had once been blocked by a proxy — or one installed before 0.23.1 — stayed on
|
|
46
|
+
the librosa-only path for good, and silently. Now every session start checks
|
|
47
|
+
and repairs it, best-effort: a blocked fetch warns and leaves the install
|
|
48
|
+
working.
|
|
49
|
+
|
|
14
50
|
## [0.23.2] — 2026-09-13
|
|
15
51
|
|
|
16
52
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: yt-tools-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.1
|
|
4
4
|
Summary: CLI suite for iterative YouTube watching by LLM agents: transcript with [mm:ss] anchors, targeted frame extraction, FFT audio analysis (BPM, key, chord progression, spectral features), and OCR over cached frames for silent-with-text videos.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kzntsv-dev/yt-tools
|
|
6
6
|
Project-URL: Repository, https://github.com/kzntsv-dev/yt-tools
|
|
@@ -84,7 +84,7 @@ use in any environment). PyPI distribution is deferred to a future release.
|
|
|
84
84
|
|
|
85
85
|
```text
|
|
86
86
|
/plugin marketplace add kzntsv-dev/claude-plugins
|
|
87
|
-
/plugin install yt-tools@
|
|
87
|
+
/plugin install yt-tools@kzntsv-dev-claude-plugins
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
The plugin's `SessionStart` hook installs the package from its own clone —
|
|
@@ -42,7 +42,7 @@ use in any environment). PyPI distribution is deferred to a future release.
|
|
|
42
42
|
|
|
43
43
|
```text
|
|
44
44
|
/plugin marketplace add kzntsv-dev/claude-plugins
|
|
45
|
-
/plugin install yt-tools@
|
|
45
|
+
/plugin install yt-tools@kzntsv-dev-claude-plugins
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
The plugin's `SessionStart` hook installs the package from its own clone —
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yt-tools-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.24.1"
|
|
8
8
|
description = "CLI suite for iterative YouTube watching by LLM agents: transcript with [mm:ss] anchors, targeted frame extraction, FFT audio analysis (BPM, key, chord progression, spectral features), and OCR over cached frames for silent-with-text videos."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<3.13"
|
|
@@ -24,6 +24,28 @@ function Write-PluginLog {
|
|
|
24
24
|
# exactly that). It is injected on top of the install instead, best-effort.
|
|
25
25
|
$BpmDetectorSpec = 'bpm-detector @ git+https://github.com/libraz/bpm-detector@v1.1.0'
|
|
26
26
|
|
|
27
|
+
function Inject-BpmDetector {
|
|
28
|
+
Invoke-Pipx inject yt-tools-cli $BpmDetectorSpec 2>&1 | ForEach-Object { Write-PluginLog $_ }
|
|
29
|
+
if ($LASTEXITCODE -ne 0) {
|
|
30
|
+
Write-PluginLog 'WARN: bpm-detector inject failed (VCS fetch blocked?); yt-listen runs the librosa-only path.'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Is the enrichment missing from an otherwise healthy install? Ask the CLI -
|
|
35
|
+
# `doctor` owns the check name and the fix command, so the hook cannot drift
|
|
36
|
+
# into disagreeing with what the agent is told. No CLI found (or an older
|
|
37
|
+
# install): stay quiet and do nothing.
|
|
38
|
+
function Test-BpmDetectorMissing {
|
|
39
|
+
$ytBin = (Get-Command yt-tools -ErrorAction SilentlyContinue).Source
|
|
40
|
+
if (-not $ytBin) {
|
|
41
|
+
$candidate = Join-Path $HOME '.local/bin/yt-tools'
|
|
42
|
+
if (Test-Path $candidate) { $ytBin = $candidate }
|
|
43
|
+
}
|
|
44
|
+
if (-not $ytBin) { return $false }
|
|
45
|
+
$report = & $ytBin doctor 2>$null
|
|
46
|
+
return [bool]($report | Select-String -Pattern '^\s*warn\s+enrichment:bpm-detector')
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
# 0. Resolve plugin root ───────────────────────────────────────────────────
|
|
28
50
|
$PluginRoot = $env:CLAUDE_PLUGIN_ROOT
|
|
29
51
|
if (-not $PluginRoot) {
|
|
@@ -171,13 +193,20 @@ if ($needsInstall) {
|
|
|
171
193
|
# enrichment is chord/structure/BPM refinement, not a flow of its own, and
|
|
172
194
|
# yt-listen already renders the librosa-only path with explicit n/a markers.
|
|
173
195
|
if ($installOk) {
|
|
174
|
-
|
|
175
|
-
if ($LASTEXITCODE -ne 0) {
|
|
176
|
-
Write-PluginLog 'WARN: bpm-detector inject failed (VCS fetch blocked?); yt-listen runs the librosa-only path.'
|
|
177
|
-
}
|
|
196
|
+
Inject-BpmDetector
|
|
178
197
|
}
|
|
179
198
|
}
|
|
180
199
|
|
|
200
|
+
# 2b. bpm-detector on an already-correct install -----------------------------
|
|
201
|
+
# The enrichment lives outside the install spec, so a version match - the common
|
|
202
|
+
# case on every later session start - never enters the block above, and a machine
|
|
203
|
+
# that once lost the VCS fetch would stay on the librosa-only path forever,
|
|
204
|
+
# silently. Probe and top up instead.
|
|
205
|
+
if (-not $needsInstall -and (Test-BpmDetectorMissing)) {
|
|
206
|
+
Write-PluginLog 'bpm-detector is missing from an up-to-date install; injecting it.'
|
|
207
|
+
Inject-BpmDetector
|
|
208
|
+
}
|
|
209
|
+
|
|
181
210
|
# 3. Probe ffmpeg ──────────────────────────────────────────────────────────
|
|
182
211
|
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
|
183
212
|
Write-PluginLog 'ffmpeg not on PATH. Install via winget:'
|
|
@@ -23,6 +23,25 @@ log() { printf '[yt-tools] %s\n' "$*" >&2; }
|
|
|
23
23
|
# exactly that). It is injected on top of the install instead, best-effort.
|
|
24
24
|
BPM_DETECTOR_SPEC="bpm-detector @ git+https://github.com/libraz/bpm-detector@v1.1.0"
|
|
25
25
|
|
|
26
|
+
inject_bpm_detector() {
|
|
27
|
+
if ! pipx_run inject yt-tools-cli "${BPM_DETECTOR_SPEC}" >&2; then
|
|
28
|
+
log "WARN: bpm-detector inject failed (VCS fetch blocked?); yt-listen runs the librosa-only path."
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Is the enrichment missing from an otherwise healthy install? Ask the CLI —
|
|
33
|
+
# `doctor` owns the check name and the fix command, so the hook cannot drift
|
|
34
|
+
# into disagreeing with what the agent is told. No CLI on PATH (or an older
|
|
35
|
+
# install): stay quiet and do nothing.
|
|
36
|
+
needs_bpm_detector() {
|
|
37
|
+
yt_bin="$(command -v yt-tools 2>/dev/null || true)"
|
|
38
|
+
if [ -z "$yt_bin" ] && [ -x "$HOME/.local/bin/yt-tools" ]; then
|
|
39
|
+
yt_bin="$HOME/.local/bin/yt-tools"
|
|
40
|
+
fi
|
|
41
|
+
[ -n "$yt_bin" ] || return 1
|
|
42
|
+
"$yt_bin" doctor 2>/dev/null | grep -Eq '^[[:space:]]*warn[[:space:]]+enrichment:bpm-detector'
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
# 0. Resolve plugin root ───────────────────────────────────────────────────
|
|
27
46
|
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
|
|
28
47
|
if [ -z "$PLUGIN_ROOT" ]; then
|
|
@@ -135,12 +154,20 @@ if [ "$needs_install" = "true" ]; then
|
|
|
135
154
|
# enrichment is chord/structure/BPM refinement, not a flow of its own, and
|
|
136
155
|
# yt-listen already renders the librosa-only path with explicit n/a markers.
|
|
137
156
|
if [ "$install_ok" = "true" ]; then
|
|
138
|
-
|
|
139
|
-
log "WARN: bpm-detector inject failed (VCS fetch blocked?); yt-listen runs the librosa-only path."
|
|
140
|
-
fi
|
|
157
|
+
inject_bpm_detector
|
|
141
158
|
fi
|
|
142
159
|
fi
|
|
143
160
|
|
|
161
|
+
# 2b. bpm-detector on an already-correct install ──────────────────────────
|
|
162
|
+
# The enrichment lives outside the install spec, so a version match — the common
|
|
163
|
+
# case on every later session start — never enters the block above, and a machine
|
|
164
|
+
# that once lost the VCS fetch would stay on the librosa-only path forever,
|
|
165
|
+
# silently. Probe and top up instead.
|
|
166
|
+
if [ "$needs_install" = "false" ] && needs_bpm_detector; then
|
|
167
|
+
log "bpm-detector is missing from an up-to-date install; injecting it."
|
|
168
|
+
inject_bpm_detector
|
|
169
|
+
fi
|
|
170
|
+
|
|
144
171
|
# 3. Probe ffmpeg ──────────────────────────────────────────────────────────
|
|
145
172
|
if ! command -v ffmpeg >/dev/null 2>&1; then
|
|
146
173
|
case "$(uname -s 2>/dev/null || echo unknown)" in
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: using-yt-tools
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.24.1
|
|
4
4
|
description: Seven flows for YouTube content. **Discovery-search** (`yt-search`) — query → markdown list of candidate videos via yt-dlp's native ytsearch extractor; the entry point when the user hasn't named a URL yet. **Iterative-watch** (summary / exploration) — transcript with [mm:ss] anchors → pick moments → extract frames. **Targeted-frames** (specific timestamps) — extract frames directly, no transcript. **Audio-analysis** (music FFT) — per timestamp spectrogram + numeric digest (BPM, key, chord progression, harmonic content) via `yt-listen`. **Metadata** (`yt-meta`, free — rides the same yt-dlp call) — description, chapters, most-replayed heatmap, view/like/comment counts, tags, subtitle languages as markdown. **Comments** (`yt-comments`, separate paginated scrape) — top comments + nested replies; capped top-50 by default, costly on viral videos, only on explicit request. **OCR** (`yt-ocr`, silent-with-text fallback) — RapidOCR PP-OCRv5 over cached frames → `[mm:ss]` blocks as a transcript substitute when `yt-transcript` is empty or the video is silent-instructional (burned-in captions, schematic labels, chord matrices). Triggers (mixed RU/EN — same skill serves both audiences) — "find a video about X", "search youtube for X", "find tutorial about X", "найди видео про X", "поищи туториал", "обзор на X youtube", "what's in this video", "video summary", "youtube transcript", "что в ролике", "о чём видео", "show frame at N", "покажи кадр на N", "listen to fragment at N", "послушай момент N", "what's the BPM", "BPM/тональность видео", "analyze audio", "спектрограмма", "video description", "show chapters", "most replayed", "video stats", "что в описании", "покажи главы", "самые пересматриваемые", "top comments", "what are people saying", "комменты под роликом", "топ комментариев", "read text from frames", "OCR this video", "what does the caption say", "extract overlay text", "captions burned in", "прочти текст с кадров", "OCR этого видео", "silent video с текстом", or any youtube.com URL. CLI installed once per machine with pipx (`pipx install 'yt-tools-cli[full]'` adds the audio/chord stack); `yt-tools doctor` is the read-only preflight that names whatever is missing. YouTube-only — for Vimeo / Twitch / local files use other tools.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -239,7 +239,7 @@ the optional `[ocr]` extra):
|
|
|
239
239
|
pipx install 'git+https://github.com/kzntsv-dev/yt-tools.git#egg=yt-tools-cli[full]'
|
|
240
240
|
```
|
|
241
241
|
If your host ships the bundled plugin (Claude Code users: install
|
|
242
|
-
`yt-tools@
|
|
242
|
+
`yt-tools@kzntsv-dev-claude-plugins`), prefer it — its SessionStart hook
|
|
243
243
|
does the pipx install from the plugin's local clone automatically.
|
|
244
244
|
|
|
245
245
|
**ffmpeg**:
|
|
@@ -24,7 +24,13 @@ from yt_tools import cache as cache_mod
|
|
|
24
24
|
from yt_tools import cli as umbrella_cli
|
|
25
25
|
from yt_tools import doctor, extras
|
|
26
26
|
|
|
27
|
-
FULL_ENV = ("scenedetect", "cv2", "librosa", "matplotlib", "rapidocr", "onnxruntime")
|
|
27
|
+
FULL_ENV = ("scenedetect", "cv2", "librosa", "matplotlib", "rapidocr", "onnxruntime", "bpm_detector")
|
|
28
|
+
|
|
29
|
+
#: Everything a bare core install has *not* got — the extras plus the one
|
|
30
|
+
#: enrichment that cannot be an extra (not on PyPI, and PyPI rejects direct
|
|
31
|
+
#: references in metadata: release 0.23.0 died on that).
|
|
32
|
+
ENRICHMENT_MODULE = "bpm_detector"
|
|
33
|
+
NO_ENRICHMENT_ENV = tuple(m for m in FULL_ENV if m != ENRICHMENT_MODULE)
|
|
28
34
|
BOTH_BINS = ("yt-dlp", "ffmpeg")
|
|
29
35
|
|
|
30
36
|
|
|
@@ -695,6 +701,50 @@ def test_ok_report_has_no_next_step(monkeypatch, tmp_path, capsys):
|
|
|
695
701
|
assert rc == 0
|
|
696
702
|
|
|
697
703
|
|
|
704
|
+
# ---- the enrichment that cannot be a package extra --------------------------
|
|
705
|
+
#
|
|
706
|
+
# `bpm-detector` is the difference between librosa-only BPM/key and full chord
|
|
707
|
+
# progression + structure in `yt-listen`, but it is not on PyPI and PyPI rejects
|
|
708
|
+
# PEP 508 direct references in `Requires-Dist` — so it cannot ride `[full]`
|
|
709
|
+
# (release 0.23.0 died on exactly that, [[task:2824]]) and is installed with
|
|
710
|
+
# `pipx inject` instead. Nothing in the install spec guarantees it, so doctor has
|
|
711
|
+
# to name it: without this line a PyPI install has no way to learn the enrichment
|
|
712
|
+
# exists at all. Absent is a `warn` — a feature is lost, nothing is blocked (D6).
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def test_missing_bpm_detector_is_a_warn_with_the_inject_command(monkeypatch, tmp_path):
|
|
716
|
+
_only_importable(monkeypatch, *NO_ENRICHMENT_ENV)
|
|
717
|
+
report = doctor.collect(tmp_path, which=_which(BOTH_BINS), version_info=(3, 12, 1))
|
|
718
|
+
|
|
719
|
+
check = _by_name(report)["enrichment:bpm-detector"]
|
|
720
|
+
assert check.status == "warn"
|
|
721
|
+
assert check.required is False
|
|
722
|
+
assert check.fix == extras.BPM_DETECTOR_FIX
|
|
723
|
+
assert "git+https://github.com/libraz/bpm-detector" in check.fix
|
|
724
|
+
assert report.can_proceed is True
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def test_a_present_bpm_detector_is_reported_ok(monkeypatch, tmp_path):
|
|
728
|
+
_only_importable(monkeypatch, *FULL_ENV)
|
|
729
|
+
report = doctor.collect(tmp_path, which=_which(BOTH_BINS), version_info=(3, 12, 1))
|
|
730
|
+
assert _by_name(report)["enrichment:bpm-detector"].status == "ok"
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_the_enrichment_alone_becomes_the_next_step(monkeypatch, tmp_path):
|
|
734
|
+
"""Nothing else wrong: the one command worth running is the enrichment."""
|
|
735
|
+
_only_importable(monkeypatch, *NO_ENRICHMENT_ENV)
|
|
736
|
+
report = doctor.collect(tmp_path, which=_which(BOTH_BINS), version_info=(3, 12, 1))
|
|
737
|
+
assert report.next_step == extras.BPM_DETECTOR_FIX
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def test_a_missing_extra_outranks_the_enrichment(monkeypatch, tmp_path):
|
|
741
|
+
"""Priority stays: extras first (they unlock whole flows), then the warn."""
|
|
742
|
+
_only_importable(monkeypatch)
|
|
743
|
+
report = doctor.collect(tmp_path, which=_which(BOTH_BINS), version_info=(3, 12, 1))
|
|
744
|
+
assert report.next_step.startswith("pipx inject yt-tools-cli ")
|
|
745
|
+
assert "git+https" not in report.next_step
|
|
746
|
+
|
|
747
|
+
|
|
698
748
|
# ---- human output is the default --------------------------------------------
|
|
699
749
|
|
|
700
750
|
|
|
@@ -707,7 +757,7 @@ def test_human_output_names_every_check_and_the_next_step(monkeypatch, tmp_path,
|
|
|
707
757
|
out = capsys.readouterr().out
|
|
708
758
|
assert "missing" in out and "can_proceed: no" in out
|
|
709
759
|
assert "next_step: sudo apt install ffmpeg" in out
|
|
710
|
-
for name in ("python", "yt-dlp", "ffmpeg", "extra:frames", "extra:audio", "extra:ocr", "cache", "version"):
|
|
760
|
+
for name in ("python", "yt-dlp", "ffmpeg", "extra:frames", "extra:audio", "extra:ocr", "enrichment:bpm-detector", "cache", "version"):
|
|
711
761
|
assert name in out
|
|
712
762
|
assert rc == 1
|
|
713
763
|
|
|
@@ -17,6 +17,7 @@ import ast
|
|
|
17
17
|
import json
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
|
+
import shutil
|
|
20
21
|
import subprocess
|
|
21
22
|
import sys
|
|
22
23
|
from pathlib import Path
|
|
@@ -95,6 +96,22 @@ HOOK_BPM_INJECT: dict[str, tuple[str, ...]] = {
|
|
|
95
96
|
),
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
# The self-heal half: the enrichment lives *outside* the install spec, so a
|
|
100
|
+
# version match — the common case on every later session start — skips the
|
|
101
|
+
# install branch entirely and nothing would ever put it back. The hook asks the
|
|
102
|
+
# CLI itself (`doctor`), whose report carries the same check name and the same
|
|
103
|
+
# fix command, so the two cannot drift into disagreeing about what is installed.
|
|
104
|
+
HOOK_BPM_SELFHEAL: dict[str, tuple[str, ...]] = {
|
|
105
|
+
"ensure-install.sh": (
|
|
106
|
+
r'command\s+-v\s+yt-tools',
|
|
107
|
+
r'enrichment:bpm-detector',
|
|
108
|
+
),
|
|
109
|
+
"ensure-install.ps1": (
|
|
110
|
+
r'Get-Command\s+yt-tools',
|
|
111
|
+
r'enrichment:bpm-detector',
|
|
112
|
+
),
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
# The extras a hook script must pass to its *actual* pipx call. Patterns, not
|
|
99
116
|
# substrings: the log lines around these commands also mention `[full]` and
|
|
100
117
|
# `[frames,audio]`, so a `in text` check stays green with the command deleted
|
|
@@ -483,6 +500,25 @@ def test_hook_injects_bpm_detector_best_effort(script_name):
|
|
|
483
500
|
)
|
|
484
501
|
|
|
485
502
|
|
|
503
|
+
@pytest.mark.parametrize("script_name", HOOK_SCRIPTS)
|
|
504
|
+
def test_hook_tops_up_bpm_detector_when_the_version_already_matches(script_name):
|
|
505
|
+
"""The gap the inject alone leaves open.
|
|
506
|
+
|
|
507
|
+
`bpm-detector` cannot be part of `[full]` (it is not on PyPI and PyPI rejects
|
|
508
|
+
direct references in metadata), so nothing in the install spec guarantees it.
|
|
509
|
+
The inject above only runs after a reinstall — which happens once, on a
|
|
510
|
+
version change. Every later session start takes the "version matches" path
|
|
511
|
+
and would leave a machine that once lost the VCS fetch (corporate proxy) on
|
|
512
|
+
the librosa-only path forever, quietly. So the hook asks the CLI.
|
|
513
|
+
"""
|
|
514
|
+
code = _code_lines((ROOT / "scripts" / script_name).read_text(encoding="utf-8"))
|
|
515
|
+
for pattern in HOOK_BPM_SELFHEAL[script_name]:
|
|
516
|
+
assert re.search(pattern, code), (
|
|
517
|
+
f"{script_name}: no self-heal probe for the enrichment (no match for {pattern!r}); "
|
|
518
|
+
"a version match skips the install branch and the inject with it"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
486
522
|
@pytest.mark.parametrize("script_name", HOOK_SCRIPTS)
|
|
487
523
|
def test_hook_pins_bpm_detector_to_the_url_requirement_46_records(script_name):
|
|
488
524
|
"""The spec lives in the hook now, so the URL is asserted there — verbatim."""
|
|
@@ -492,6 +528,42 @@ def test_hook_pins_bpm_detector_to_the_url_requirement_46_records(script_name):
|
|
|
492
528
|
)
|
|
493
529
|
|
|
494
530
|
|
|
531
|
+
def test_hook_shell_script_parses():
|
|
532
|
+
"""Content guards do not see syntax.
|
|
533
|
+
|
|
534
|
+
A stray `fi` (from a restructure that moved a block out of an `if`) passed
|
|
535
|
+
every regex above and every unit test — the packaging suite asserts strings,
|
|
536
|
+
not grammar. It surfaced only when the hook was run by hand. `bash -n` is the
|
|
537
|
+
cheap check that sees it, and it costs milliseconds on every push.
|
|
538
|
+
"""
|
|
539
|
+
bash = shutil.which("bash")
|
|
540
|
+
if bash is None: # pragma: no cover - Windows without Git Bash
|
|
541
|
+
pytest.skip("no bash on PATH")
|
|
542
|
+
proc = subprocess.run(
|
|
543
|
+
[bash, "-n", str(ROOT / "scripts" / "ensure-install.sh")],
|
|
544
|
+
capture_output=True,
|
|
545
|
+
text=True,
|
|
546
|
+
)
|
|
547
|
+
assert proc.returncode == 0, f"ensure-install.sh does not parse:\n{proc.stderr}"
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def test_hook_powershell_script_parses():
|
|
551
|
+
"""Same guard for the Windows hook (skipped where no PowerShell exists)."""
|
|
552
|
+
shell = shutil.which("pwsh") or shutil.which("powershell")
|
|
553
|
+
if shell is None: # pragma: no cover - Linux/macOS CI
|
|
554
|
+
pytest.skip("no PowerShell on PATH")
|
|
555
|
+
script = str(ROOT / "scripts" / "ensure-install.ps1")
|
|
556
|
+
parse = (
|
|
557
|
+
f"$errors = $null; "
|
|
558
|
+
f"[void][System.Management.Automation.Language.Parser]::ParseFile('{script}', [ref]$null, [ref]$errors); "
|
|
559
|
+
f"if ($errors.Count) {{ $errors | ForEach-Object {{ Write-Error $_.Message }}; exit 1 }}"
|
|
560
|
+
)
|
|
561
|
+
proc = subprocess.run(
|
|
562
|
+
[shell, "-NoProfile", "-Command", parse], capture_output=True, text=True
|
|
563
|
+
)
|
|
564
|
+
assert proc.returncode == 0, f"ensure-install.ps1 does not parse:\n{proc.stderr}"
|
|
565
|
+
|
|
566
|
+
|
|
495
567
|
# ---- documentation ----------------------------------------------------------
|
|
496
568
|
|
|
497
569
|
|
|
@@ -213,6 +213,30 @@ def ffmpeg_fix(platform_name: str) -> str:
|
|
|
213
213
|
return FFMPEG_FIX.get(platform_name, FFMPEG_FIX_FALLBACK)
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def check_bpm_detector() -> Check:
|
|
217
|
+
"""The one enrichment that cannot be an extra — named anyway.
|
|
218
|
+
|
|
219
|
+
It does not go through ``check_extra`` because there is no ``[extra]`` to
|
|
220
|
+
install: the package is not on PyPI, so its absence is only fixable by
|
|
221
|
+
``pipx inject``. For the caller it is the same kind of absence as a missing
|
|
222
|
+
extra — a feature is lost, nothing is blocked — so it is reported the same
|
|
223
|
+
way, with the exact command (D6). Without this line a PyPI install has no
|
|
224
|
+
way to learn the enrichment exists at all.
|
|
225
|
+
"""
|
|
226
|
+
if extras.module_missing(extras.BPM_DETECTOR_MODULE):
|
|
227
|
+
return Check(
|
|
228
|
+
"enrichment:bpm-detector",
|
|
229
|
+
WARN,
|
|
230
|
+
"not installed - yt-listen falls back to librosa-only BPM/key",
|
|
231
|
+
fix=extras.BPM_DETECTOR_FIX,
|
|
232
|
+
)
|
|
233
|
+
return Check(
|
|
234
|
+
"enrichment:bpm-detector",
|
|
235
|
+
OK,
|
|
236
|
+
"installed - chord progression + structure in yt-listen",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
216
240
|
def check_extra(extra: str) -> Check:
|
|
217
241
|
"""Is the heavy stack for one flow importable here (D4's probe, not a spec)?"""
|
|
218
242
|
if extras.extra_available(extra):
|
|
@@ -431,6 +455,7 @@ def collect(
|
|
|
431
455
|
"ffmpeg", fix=ffmpeg_fix(platform_name or sys.platform), which=which
|
|
432
456
|
),
|
|
433
457
|
*(check_extra(extra) for extra in extras.EXTRA_MODULES),
|
|
458
|
+
check_bpm_detector(),
|
|
434
459
|
check_cache(cache_root(base)),
|
|
435
460
|
check_version(
|
|
436
461
|
package_version=__version__,
|
|
@@ -41,6 +41,17 @@ EXTRA_PACKAGES: dict[str, tuple[str, ...]] = {
|
|
|
41
41
|
"ocr": ("rapidocr", "onnxruntime"),
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
#: The enrichment that cannot be a package extra. ``bpm-detector`` gives
|
|
45
|
+
#: ``yt-listen`` chord progression, structural segments and a refined BPM/key,
|
|
46
|
+
#: but it is not on PyPI — so it can only be a PEP 508 direct reference, and PyPI
|
|
47
|
+
#: rejects those in ``Requires-Dist`` (release 0.23.0 died on exactly that,
|
|
48
|
+
#: [[task:2824]]). It is therefore installed with ``pipx inject``: by the plugin
|
|
49
|
+
#: hook, or by hand from the README. ``doctor`` names it for the same reason it
|
|
50
|
+
#: names a missing extra — a silent quality drop is worse than a named one (D6).
|
|
51
|
+
BPM_DETECTOR_MODULE = "bpm_detector"
|
|
52
|
+
BPM_DETECTOR_SPEC = "bpm-detector @ git+https://github.com/libraz/bpm-detector@v1.1.0"
|
|
53
|
+
BPM_DETECTOR_FIX = f'pipx inject {DISTRIBUTION} "{BPM_DETECTOR_SPEC}"'
|
|
54
|
+
|
|
44
55
|
_MISSING = object()
|
|
45
56
|
|
|
46
57
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|