ripdoctor 0.1.0__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.
- ripdoctor-0.1.0/.agent-standard +3 -0
- ripdoctor-0.1.0/.github/dependabot.yml +30 -0
- ripdoctor-0.1.0/.github/rulesets/main.json +55 -0
- ripdoctor-0.1.0/.github/workflows/ci.yml +76 -0
- ripdoctor-0.1.0/.github/workflows/release.yml +85 -0
- ripdoctor-0.1.0/.gitignore +30 -0
- ripdoctor-0.1.0/LICENSE +21 -0
- ripdoctor-0.1.0/Makefile +76 -0
- ripdoctor-0.1.0/PKG-INFO +269 -0
- ripdoctor-0.1.0/README.md +243 -0
- ripdoctor-0.1.0/docs/decisions.md +1418 -0
- ripdoctor-0.1.0/docs/images/cut_panel.png +0 -0
- ripdoctor-0.1.0/docs/images/rip_panel.png +0 -0
- ripdoctor-0.1.0/docs/method.md +265 -0
- ripdoctor-0.1.0/packaging/ripdoctor.service +65 -0
- ripdoctor-0.1.0/pyproject.toml +86 -0
- ripdoctor-0.1.0/ripdoctor/__init__.py +23 -0
- ripdoctor-0.1.0/ripdoctor/audio/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/audio/align.py +249 -0
- ripdoctor-0.1.0/ripdoctor/audio/astats.py +148 -0
- ripdoctor-0.1.0/ripdoctor/audio/capture.py +309 -0
- ripdoctor-0.1.0/ripdoctor/audio/decode.py +92 -0
- ripdoctor-0.1.0/ripdoctor/audio/devices.py +136 -0
- ripdoctor-0.1.0/ripdoctor/audio/ffprobe.py +81 -0
- ripdoctor-0.1.0/ripdoctor/audio/passthru.py +241 -0
- ripdoctor-0.1.0/ripdoctor/audio/runner.py +360 -0
- ripdoctor-0.1.0/ripdoctor/audio/session.py +197 -0
- ripdoctor-0.1.0/ripdoctor/audio/split.py +154 -0
- ripdoctor-0.1.0/ripdoctor/cli.py +661 -0
- ripdoctor-0.1.0/ripdoctor/config/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/config/machine.py +75 -0
- ripdoctor-0.1.0/ripdoctor/config/settings.py +209 -0
- ripdoctor-0.1.0/ripdoctor/config/thresholds.py +56 -0
- ripdoctor-0.1.0/ripdoctor/core/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/core/autostop.py +106 -0
- ripdoctor-0.1.0/ripdoctor/core/envelope.py +194 -0
- ripdoctor-0.1.0/ripdoctor/core/fit.py +247 -0
- ripdoctor-0.1.0/ripdoctor/core/gaps.py +181 -0
- ripdoctor-0.1.0/ripdoctor/core/measure.py +179 -0
- ripdoctor-0.1.0/ripdoctor/core/meter.py +175 -0
- ripdoctor-0.1.0/ripdoctor/core/naming.py +116 -0
- ripdoctor-0.1.0/ripdoctor/core/plan.py +222 -0
- ripdoctor-0.1.0/ripdoctor/core/sides.py +104 -0
- ripdoctor-0.1.0/ripdoctor/core/xcorr.py +133 -0
- ripdoctor-0.1.0/ripdoctor/doctor/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/doctor/checks.py +332 -0
- ripdoctor-0.1.0/ripdoctor/integrations/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/integrations/artwork.py +293 -0
- ripdoctor-0.1.0/ripdoctor/integrations/importer.py +310 -0
- ripdoctor-0.1.0/ripdoctor/integrations/musicbrainz.py +292 -0
- ripdoctor-0.1.0/ripdoctor/integrations/tagger.py +197 -0
- ripdoctor-0.1.0/ripdoctor/store/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/store/archive.py +179 -0
- ripdoctor-0.1.0/ripdoctor/store/cache.py +281 -0
- ripdoctor-0.1.0/ripdoctor/store/files.py +333 -0
- ripdoctor-0.1.0/ripdoctor/store/safety.py +34 -0
- ripdoctor-0.1.0/ripdoctor/web/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/web/app.py +96 -0
- ripdoctor-0.1.0/ripdoctor/web/auth.py +237 -0
- ripdoctor-0.1.0/ripdoctor/web/http.py +196 -0
- ripdoctor-0.1.0/ripdoctor/web/httpd.py +213 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/__init__.py +27 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/art.py +145 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/capture.py +468 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/cutting.py +140 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/library.py +390 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/punching.py +130 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/records.py +394 -0
- ripdoctor-0.1.0/ripdoctor/web/routes/session.py +45 -0
- ripdoctor-0.1.0/ripdoctor/web/service.py +40 -0
- ripdoctor-0.1.0/ripdoctor/web/static/app.js +2098 -0
- ripdoctor-0.1.0/ripdoctor/web/static/audio.js +97 -0
- ripdoctor-0.1.0/ripdoctor/web/static/editor.js +355 -0
- ripdoctor-0.1.0/ripdoctor/web/static/icon.svg +39 -0
- ripdoctor-0.1.0/ripdoctor/web/static/index.html +329 -0
- ripdoctor-0.1.0/ripdoctor/web/static/ripui.js +80 -0
- ripdoctor-0.1.0/ripdoctor/web/static/style.css +274 -0
- ripdoctor-0.1.0/ripdoctor/web/static.py +55 -0
- ripdoctor-0.1.0/ripdoctor/work/__init__.py +1 -0
- ripdoctor-0.1.0/ripdoctor/work/capture.py +203 -0
- ripdoctor-0.1.0/ripdoctor/work/jobs.py +138 -0
- ripdoctor-0.1.0/ripdoctor/work/punch.py +336 -0
- ripdoctor-0.1.0/scripts/budgets.py +165 -0
- ripdoctor-0.1.0/tests/__init__.py +0 -0
- ripdoctor-0.1.0/tests/conftest.py +39 -0
- ripdoctor-0.1.0/tests/fixtures/golden/manifest.json +38 -0
- ripdoctor-0.1.0/tests/fixtures/golden/record-30deab76.json +383 -0
- ripdoctor-0.1.0/tests/fixtures/golden/record-389fec66.json +571 -0
- ripdoctor-0.1.0/tests/fixtures/golden/record-910f935f.json +1 -0
- ripdoctor-0.1.0/tests/fixtures/golden/record-a5b4a005.json +1 -0
- ripdoctor-0.1.0/tests/fixtures/golden/record-c5652c48.json +337 -0
- ripdoctor-0.1.0/tests/fixtures/golden/record-f838e3f8.json +493 -0
- ripdoctor-0.1.0/tests/fixtures/plans/ear-set.plan.json +99 -0
- ripdoctor-0.1.0/tests/fixtures/plans/ear-set.spec.json +117 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-1bc1c97e-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-1bc1c97e-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-1bc1c97e-side-c.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-1bc1c97e-side-d.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-30deab76-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-30deab76-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-52578964-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-52578964-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-52578964-side-c.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-52578964-side-d.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-910f935f-side-Orphan.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-910f935f-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-910f935f-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-949564fd-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-949564fd-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-a5b4a005-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-a5b4a005-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-a5b4a005-side-c.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-a5b4a005-side-d.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-b098cc91-side-Sample.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-b098cc91-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-c5652c48-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-c5652c48-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-c5652c48-side-c.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-c5652c48-side-d.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-d68a2806-side-a.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-d68a2806-side-b.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-d68a2806-side-c.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/album-d68a2806-side-d.env +0 -0
- ripdoctor-0.1.0/tests/fixtures/sides/manifest.json +147 -0
- ripdoctor-0.1.0/tests/pool.py +77 -0
- ripdoctor-0.1.0/tests/tape.py +102 -0
- ripdoctor-0.1.0/tests/test_align.py +190 -0
- ripdoctor-0.1.0/tests/test_architecture.py +165 -0
- ripdoctor-0.1.0/tests/test_artwork.py +214 -0
- ripdoctor-0.1.0/tests/test_audio_decode.py +181 -0
- ripdoctor-0.1.0/tests/test_audio_probe.py +281 -0
- ripdoctor-0.1.0/tests/test_audio_split.py +188 -0
- ripdoctor-0.1.0/tests/test_autostop.py +226 -0
- ripdoctor-0.1.0/tests/test_beets_live.py +156 -0
- ripdoctor-0.1.0/tests/test_cache.py +213 -0
- ripdoctor-0.1.0/tests/test_capture.py +369 -0
- ripdoctor-0.1.0/tests/test_cli.py +694 -0
- ripdoctor-0.1.0/tests/test_config.py +212 -0
- ripdoctor-0.1.0/tests/test_devices.py +113 -0
- ripdoctor-0.1.0/tests/test_doctor.py +357 -0
- ripdoctor-0.1.0/tests/test_end_to_end.py +168 -0
- ripdoctor-0.1.0/tests/test_envelope.py +295 -0
- ripdoctor-0.1.0/tests/test_fit.py +367 -0
- ripdoctor-0.1.0/tests/test_frontend.py +147 -0
- ripdoctor-0.1.0/tests/test_gaps.py +268 -0
- ripdoctor-0.1.0/tests/test_golden.py +270 -0
- ripdoctor-0.1.0/tests/test_importer.py +288 -0
- ripdoctor-0.1.0/tests/test_jobs.py +161 -0
- ripdoctor-0.1.0/tests/test_measure.py +124 -0
- ripdoctor-0.1.0/tests/test_meter.py +212 -0
- ripdoctor-0.1.0/tests/test_musicbrainz.py +312 -0
- ripdoctor-0.1.0/tests/test_naming.py +180 -0
- ripdoctor-0.1.0/tests/test_passthru.py +196 -0
- ripdoctor-0.1.0/tests/test_plan.py +202 -0
- ripdoctor-0.1.0/tests/test_punch.py +304 -0
- ripdoctor-0.1.0/tests/test_runner.py +146 -0
- ripdoctor-0.1.0/tests/test_session.py +252 -0
- ripdoctor-0.1.0/tests/test_sides.py +172 -0
- ripdoctor-0.1.0/tests/test_store.py +436 -0
- ripdoctor-0.1.0/tests/test_tagger.py +213 -0
- ripdoctor-0.1.0/tests/test_web_app.py +183 -0
- ripdoctor-0.1.0/tests/test_web_auth.py +219 -0
- ripdoctor-0.1.0/tests/test_web_capture.py +701 -0
- ripdoctor-0.1.0/tests/test_web_contract.py +172 -0
- ripdoctor-0.1.0/tests/test_web_http.py +140 -0
- ripdoctor-0.1.0/tests/test_web_httpd.py +248 -0
- ripdoctor-0.1.0/tests/test_web_library.py +651 -0
- ripdoctor-0.1.0/tests/test_web_punch.py +213 -0
- ripdoctor-0.1.0/tests/test_web_routes.py +799 -0
- ripdoctor-0.1.0/tests/test_work_capture.py +141 -0
- ripdoctor-0.1.0/tests/test_xcorr.py +181 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# One dependency and a handful of actions, all of which move without telling
|
|
2
|
+
# anyone. A pull request that has to pass the same gates as anything else is a
|
|
3
|
+
# reasonable way to hear about it; an automatic merge would not be.
|
|
4
|
+
version: 2
|
|
5
|
+
updates:
|
|
6
|
+
- package-ecosystem: pip
|
|
7
|
+
directory: /
|
|
8
|
+
schedule:
|
|
9
|
+
# Mondays, early. Nothing here is urgent, and weekly is often enough to
|
|
10
|
+
# catch a release without the tracker becoming wallpaper.
|
|
11
|
+
interval: weekly
|
|
12
|
+
day: monday
|
|
13
|
+
time: "06:00"
|
|
14
|
+
open-pull-requests-limit: 3
|
|
15
|
+
groups:
|
|
16
|
+
# The dev tools move together and are reviewed together; one pull request
|
|
17
|
+
# for a ruff and a mypy bump is one review, not two.
|
|
18
|
+
tooling:
|
|
19
|
+
patterns: ["ruff", "mypy", "pytest*", "hypothesis"]
|
|
20
|
+
|
|
21
|
+
- package-ecosystem: github-actions
|
|
22
|
+
directory: /
|
|
23
|
+
schedule:
|
|
24
|
+
interval: weekly
|
|
25
|
+
day: monday
|
|
26
|
+
time: "06:00"
|
|
27
|
+
open-pull-requests-limit: 3
|
|
28
|
+
groups:
|
|
29
|
+
actions:
|
|
30
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "main",
|
|
3
|
+
"target": "branch",
|
|
4
|
+
"enforcement": "active",
|
|
5
|
+
"conditions": {
|
|
6
|
+
"ref_name": {
|
|
7
|
+
"exclude": [],
|
|
8
|
+
"include": ["~DEFAULT_BRANCH"]
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"rules": [
|
|
12
|
+
{ "type": "deletion" },
|
|
13
|
+
{ "type": "non_fast_forward" },
|
|
14
|
+
{ "type": "required_linear_history" },
|
|
15
|
+
{
|
|
16
|
+
"type": "pull_request",
|
|
17
|
+
"parameters": {
|
|
18
|
+
"required_approving_review_count": 0,
|
|
19
|
+
"dismiss_stale_reviews_on_push": true,
|
|
20
|
+
"required_reviewers": [],
|
|
21
|
+
"require_code_owner_review": false,
|
|
22
|
+
"require_last_push_approval": false,
|
|
23
|
+
"required_review_thread_resolution": false,
|
|
24
|
+
"allowed_merge_methods": ["squash", "rebase"]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "required_status_checks",
|
|
29
|
+
"parameters": {
|
|
30
|
+
"strict_required_status_checks_policy": true,
|
|
31
|
+
"do_not_enforce_on_create": false,
|
|
32
|
+
"required_status_checks": [
|
|
33
|
+
{ "context": "check (3.11)", "integration_id": 15368 },
|
|
34
|
+
{ "context": "check (3.12)", "integration_id": 15368 },
|
|
35
|
+
{ "context": "check (3.13)", "integration_id": 15368 },
|
|
36
|
+
{ "context": "with-tools", "integration_id": 15368 },
|
|
37
|
+
{ "context": "packaging", "integration_id": 15368 }
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "code_scanning",
|
|
43
|
+
"parameters": {
|
|
44
|
+
"code_scanning_tools": [
|
|
45
|
+
{
|
|
46
|
+
"tool": "CodeQL",
|
|
47
|
+
"security_alerts_threshold": "high_or_higher",
|
|
48
|
+
"alerts_threshold": "errors"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"bypass_actors": []
|
|
55
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
check:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python: ["3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0 # hatch-vcs derives the version from tags
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python }}
|
|
24
|
+
- run: pip install -e ".[dev]"
|
|
25
|
+
- run: python -m ruff format --check .
|
|
26
|
+
- run: python -m ruff check .
|
|
27
|
+
- run: python -m mypy
|
|
28
|
+
- run: python -m pytest
|
|
29
|
+
- run: python scripts/budgets.py
|
|
30
|
+
|
|
31
|
+
# The tier that needs real external programs. It asserts the marked tests
|
|
32
|
+
# actually ran: a filter that matches nothing exits zero, so a job meant to
|
|
33
|
+
# run this tier and running none of it would otherwise report green.
|
|
34
|
+
with-tools:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
with:
|
|
39
|
+
fetch-depth: 0
|
|
40
|
+
- uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: "3.12"
|
|
43
|
+
# flac carries metaflac; beets arrives as a dependency of the package.
|
|
44
|
+
- run: sudo apt-get update && sudo apt-get install -y ffmpeg flac
|
|
45
|
+
- run: pip install -e ".[dev]"
|
|
46
|
+
- run: python -m pytest -q --require=needs_ffmpeg --require=needs_beets
|
|
47
|
+
|
|
48
|
+
# Python's failure mode is not a compile error, it is a wheel that will not
|
|
49
|
+
# install. Build it, check it, install it clean, and run the binary.
|
|
50
|
+
packaging:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
with:
|
|
55
|
+
fetch-depth: 0
|
|
56
|
+
- uses: actions/setup-python@v5
|
|
57
|
+
with:
|
|
58
|
+
python-version: "3.11"
|
|
59
|
+
- run: pip install build twine
|
|
60
|
+
- run: python -m build
|
|
61
|
+
- run: python -m twine check dist/*
|
|
62
|
+
- name: install the wheel into a clean environment
|
|
63
|
+
run: |
|
|
64
|
+
python -m venv /tmp/clean
|
|
65
|
+
/tmp/clean/bin/pip install dist/*.whl
|
|
66
|
+
/tmp/clean/bin/ripdoctor --version
|
|
67
|
+
/tmp/clean/bin/ripdoctor doctor --json > /dev/null || true
|
|
68
|
+
# The install is allowed beets and whatever beets itself needs. What it
|
|
69
|
+
# is not allowed is a dependency of this project's own that nobody
|
|
70
|
+
# decided on - scripts/budgets.py holds that line against pyproject.toml,
|
|
71
|
+
# and this checks the thing pip actually resolved.
|
|
72
|
+
- name: the install pulls in what it says it does
|
|
73
|
+
run: |
|
|
74
|
+
/tmp/clean/bin/pip show ripdoctor | grep -q '^Requires: beets$' || {
|
|
75
|
+
/tmp/clean/bin/pip show ripdoctor; echo "unexpected requirements"; exit 1; }
|
|
76
|
+
/tmp/clean/bin/beet version
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
# Trusted publishing and keyless signing both work the same way: id-token
|
|
10
|
+
# mints an OIDC token that binds the upload to this workflow, and there is no
|
|
11
|
+
# API token to store, rotate or leak. attestations writes the provenance.
|
|
12
|
+
id-token: write
|
|
13
|
+
attestations: write
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
release:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment: pypi
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0 # hatch-vcs derives the version from tags
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.11"
|
|
26
|
+
|
|
27
|
+
# The gates run again here rather than trusting the branch: a tag can be
|
|
28
|
+
# pushed at any commit, including one CI never saw.
|
|
29
|
+
- run: sudo apt-get update && sudo apt-get install -y ffmpeg flac
|
|
30
|
+
- run: pip install -e ".[dev]"
|
|
31
|
+
- run: python -m ruff format --check .
|
|
32
|
+
- run: python -m ruff check .
|
|
33
|
+
- run: python -m mypy
|
|
34
|
+
- run: python -m pytest -q --require=needs_ffmpeg --require=needs_beets
|
|
35
|
+
- run: python scripts/budgets.py
|
|
36
|
+
|
|
37
|
+
- run: pip install build twine
|
|
38
|
+
- run: python -m build
|
|
39
|
+
|
|
40
|
+
# hatch-vcs writes the version from the tag, so a mismatch means the tree
|
|
41
|
+
# was dirty or the tags did not arrive - either way the artifact is not
|
|
42
|
+
# the thing the tag names, and it is cheaper to find that out here than
|
|
43
|
+
# from a release nobody can reproduce.
|
|
44
|
+
- name: the artifact matches the tag
|
|
45
|
+
run: |
|
|
46
|
+
built=$(ls dist/*.whl | sed -E 's/.*ripdoctor-([^-]+)-py3.*/\1/')
|
|
47
|
+
want="${GITHUB_REF_NAME#v}"
|
|
48
|
+
test "$built" = "$want" || {
|
|
49
|
+
echo "::error::tag ${GITHUB_REF_NAME} but the wheel says $built"
|
|
50
|
+
exit 1; }
|
|
51
|
+
|
|
52
|
+
- run: python -m twine check dist/*
|
|
53
|
+
|
|
54
|
+
# Python's failure mode is not a compile error, it is a wheel that will
|
|
55
|
+
# not install. Prove it does, from the artifact about to be published.
|
|
56
|
+
- name: install the wheel into a clean environment
|
|
57
|
+
run: |
|
|
58
|
+
python -m venv /tmp/clean
|
|
59
|
+
/tmp/clean/bin/pip install dist/*.whl
|
|
60
|
+
/tmp/clean/bin/ripdoctor --version
|
|
61
|
+
/tmp/clean/bin/beet version
|
|
62
|
+
|
|
63
|
+
- name: sign the artifacts
|
|
64
|
+
id: attest
|
|
65
|
+
uses: actions/attest-build-provenance@v2
|
|
66
|
+
with:
|
|
67
|
+
subject-path: 'dist/*'
|
|
68
|
+
|
|
69
|
+
# Published beside the artifacts so verifying needs no GitHub account:
|
|
70
|
+
# `gh attestation verify` without --bundle asks the API, which requires a
|
|
71
|
+
# login; with one it reads from disk.
|
|
72
|
+
- name: publish the release
|
|
73
|
+
run: |
|
|
74
|
+
cp "${{ steps.attest.outputs.bundle-path }}" attestation.jsonl
|
|
75
|
+
gh release create "${GITHUB_REF_NAME}" \
|
|
76
|
+
--title "${GITHUB_REF_NAME}" \
|
|
77
|
+
--generate-notes \
|
|
78
|
+
dist/* attestation.jsonl
|
|
79
|
+
env:
|
|
80
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
81
|
+
|
|
82
|
+
# Last, because it is the only step that cannot be undone. Everything
|
|
83
|
+
# above can be deleted and done again; a version on PyPI cannot be
|
|
84
|
+
# replaced, only yanked.
|
|
85
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Secrets never enter this repo.
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
|
|
5
|
+
# Build and test artifacts
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
htmlcov/
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.pyc
|
|
16
|
+
|
|
17
|
+
# Virtualenvs
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
|
|
21
|
+
# Editor / OS
|
|
22
|
+
*.bak
|
|
23
|
+
*.orig
|
|
24
|
+
*.swp
|
|
25
|
+
*~
|
|
26
|
+
.DS_Store
|
|
27
|
+
|
|
28
|
+
# Maps anonymised fixtures back to album names. Never committed.
|
|
29
|
+
tests/fixtures/PRIVATE-mapping.json
|
|
30
|
+
tests/fixtures/PRIVATE-golden-mapping.json
|
ripdoctor-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bryan Speelman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ripdoctor-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# RipDoctor.
|
|
2
|
+
#
|
|
3
|
+
# `make check` is the gate. It is what CI runs and what must pass before a
|
|
4
|
+
# commit. Everything else here is a component of it or a convenience.
|
|
5
|
+
|
|
6
|
+
PY ?= .venv/bin/python
|
|
7
|
+
|
|
8
|
+
.PHONY: help venv lint types test cov budgets check wheel release clean
|
|
9
|
+
|
|
10
|
+
help:
|
|
11
|
+
@echo "make check lint, types, tests, budgets - the gate"
|
|
12
|
+
@echo "make venv create .venv and install the package editable"
|
|
13
|
+
@echo "make lint ruff format --check and ruff check"
|
|
14
|
+
@echo "make types mypy (strict on core/)"
|
|
15
|
+
@echo "make test pytest"
|
|
16
|
+
@echo "make cov pytest with coverage, gated on core/ only"
|
|
17
|
+
@echo "make budgets size budgets; failing one means retire, not raise"
|
|
18
|
+
@echo "make wheel build the wheel and check its size"
|
|
19
|
+
@echo "make release VERSION=x.y.z tag it; Actions builds and publishes"
|
|
20
|
+
|
|
21
|
+
venv:
|
|
22
|
+
python3 -m venv .venv
|
|
23
|
+
$(PY) -m pip install --upgrade pip
|
|
24
|
+
$(PY) -m pip install -e ".[dev]"
|
|
25
|
+
|
|
26
|
+
lint:
|
|
27
|
+
$(PY) -m ruff format --check .
|
|
28
|
+
$(PY) -m ruff check .
|
|
29
|
+
|
|
30
|
+
types:
|
|
31
|
+
$(PY) -m mypy
|
|
32
|
+
|
|
33
|
+
test:
|
|
34
|
+
$(PY) -m pytest
|
|
35
|
+
|
|
36
|
+
# Scoped to core/ deliberately. A whole-project coverage number lets untested
|
|
37
|
+
# application code hide behind a well-tested algorithm, which is the opposite of
|
|
38
|
+
# what the number is for.
|
|
39
|
+
cov:
|
|
40
|
+
$(PY) -m pytest --cov=ripdoctor/core --cov-report=term-missing --cov-fail-under=95
|
|
41
|
+
|
|
42
|
+
budgets:
|
|
43
|
+
$(PY) scripts/budgets.py
|
|
44
|
+
|
|
45
|
+
check: lint types test budgets
|
|
46
|
+
|
|
47
|
+
wheel:
|
|
48
|
+
$(PY) -m pip install --quiet build
|
|
49
|
+
$(PY) scripts/budgets.py --wheel
|
|
50
|
+
|
|
51
|
+
# There is no version to bump: hatch-vcs writes it from the tag, so the tag is
|
|
52
|
+
# the release. What is left is the checking that the tag is being put somewhere
|
|
53
|
+
# it can be reproduced from - a green tree, on main, level with the remote.
|
|
54
|
+
release:
|
|
55
|
+
@test -n "$(VERSION)" || { echo "usage: make release VERSION=x.y.z"; exit 1; }
|
|
56
|
+
@echo "$(VERSION)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$' || \
|
|
57
|
+
{ echo "VERSION must be x.y.z"; exit 1; }
|
|
58
|
+
@test "$$(git rev-parse --abbrev-ref HEAD)" = main || \
|
|
59
|
+
{ echo "release from main"; exit 1; }
|
|
60
|
+
@git diff --quiet && git diff --cached --quiet || \
|
|
61
|
+
{ echo "working tree is dirty; commit first"; exit 1; }
|
|
62
|
+
@! git rev-parse -q --verify "refs/tags/v$(VERSION)" >/dev/null || \
|
|
63
|
+
{ echo "v$(VERSION) already exists"; exit 1; }
|
|
64
|
+
git fetch --quiet origin main
|
|
65
|
+
@test "$$(git rev-parse HEAD)" = "$$(git rev-parse origin/main)" || \
|
|
66
|
+
{ echo "main is not level with origin/main; run 'git pull'"; exit 1; }
|
|
67
|
+
$(MAKE) check
|
|
68
|
+
git tag -a "v$(VERSION)" -m "v$(VERSION)"
|
|
69
|
+
git push origin "v$(VERSION)"
|
|
70
|
+
@echo
|
|
71
|
+
@echo "pushed v$(VERSION). Actions runs the gates again at the tag, builds,"
|
|
72
|
+
@echo "attests, publishes the release and uploads to PyPI."
|
|
73
|
+
|
|
74
|
+
clean:
|
|
75
|
+
rm -rf dist build .pytest_cache .mypy_cache .ruff_cache htmlcov .coverage
|
|
76
|
+
find . -name __pycache__ -type d -prune -exec rm -rf {} +
|
ripdoctor-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ripdoctor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Capture a vinyl side, find the track boundaries, cut them where you want them.
|
|
5
|
+
Project-URL: Homepage, https://github.com/bspeelm/ripdoctor
|
|
6
|
+
Project-URL: Source, https://github.com/bspeelm/ripdoctor
|
|
7
|
+
Author: Bryan Speelman
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: audio,digitization,flac,track-splitting,vinyl
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Capture/Recording
|
|
16
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Editors
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: beets>=2.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
21
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# RipDoctor
|
|
28
|
+
|
|
29
|
+
Capture a vinyl side, find the track boundaries, cut them where you want them.
|
|
30
|
+
|
|
31
|
+
**Status: it has taken a record.** One LP, start to finish on real hardware -
|
|
32
|
+
both sides captured from the turntable, boundaries fitted and checked, tracks
|
|
33
|
+
cut, tagged and filed with cover art and album-mode ReplayGain, and the raw
|
|
34
|
+
sides archived only once the record was provably in the library. Driven from
|
|
35
|
+
the page throughout, with nothing typed at a shell to get it through.
|
|
36
|
+
|
|
37
|
+
That is one record, on one machine, through one signal chain, by the person who
|
|
38
|
+
wrote it. It is the difference between "the tests pass" and "it works", and it
|
|
39
|
+
is not the same as "it works for you". See
|
|
40
|
+
[docs/decisions.md](docs/decisions.md) for what has been decided and why, and
|
|
41
|
+
`ripdoctor doctor` for what this machine is missing.
|
|
42
|
+
|
|
43
|
+
## The problem it solves
|
|
44
|
+
|
|
45
|
+
Splitting a recorded vinyl side into tracks is usually done by detecting
|
|
46
|
+
silence: pick a threshold in decibels, call anything quieter a gap, cut there.
|
|
47
|
+
On most records this works.
|
|
48
|
+
|
|
49
|
+
On a sparse pressing it cannot work, because there is no such threshold. A real
|
|
50
|
+
inter-track gap and a quiet passage inside a song both sit near −43 dB
|
|
51
|
+
full-band, and no number separates them. Tuning the threshold trades one failure
|
|
52
|
+
for the other. On one album this produced two tracks that were 29 seconds too
|
|
53
|
+
long and 22 seconds too short.
|
|
54
|
+
|
|
55
|
+
Restrict the measurement to 1–3 kHz and the same two things separate by about
|
|
56
|
+
20 dB. Vinyl's noise — plinth rumble, arm handling, warp — is bass-heavy, and on
|
|
57
|
+
a quiet pressing there is nothing at all above 8 kHz. The 1–3 kHz band is where
|
|
58
|
+
music is and noise is not.
|
|
59
|
+
|
|
60
|
+
| | full band | 1–3 kHz |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| music | −26 | −40 |
|
|
63
|
+
| arm up, being handled | −36 | −67 |
|
|
64
|
+
| silent groove | −64 | −85 |
|
|
65
|
+
| dead air, needle up | −89 | −93 |
|
|
66
|
+
|
|
67
|
+
Full band puts music and arm-rumble 10 dB apart. The band lane puts them 27 dB
|
|
68
|
+
apart. Every boundary decision in this tool keys off that.
|
|
69
|
+
|
|
70
|
+
These are measurements from one signal chain, not universal constants. See
|
|
71
|
+
[docs/method.md](docs/method.md) for what each number means and where it came
|
|
72
|
+
from.
|
|
73
|
+
|
|
74
|
+

|
|
76
|
+
|
|
77
|
+
That is the argument in one picture. The blue is the full-band waveform, where
|
|
78
|
+
the gaps between tracks are barely dents. The orange line under it is the same
|
|
79
|
+
side measured in 1-3 kHz, and every inter-track gap drops clean through the
|
|
80
|
+
dashed threshold. Nine of them, on a side where full-band silence detection
|
|
81
|
+
finds an ambiguous handful.
|
|
82
|
+
|
|
83
|
+
## What it does
|
|
84
|
+
|
|
85
|
+
**Records**, if the turntable is attached to the machine running it, with a live
|
|
86
|
+
meter showing the band level so you can see what the stylus is doing. It also
|
|
87
|
+
accepts a WAV or FLAC you recorded somewhere else; neither path is
|
|
88
|
+
second-class.
|
|
89
|
+
|
|
90
|
+
**Fits** boundaries by requiring two independent constraints to agree: the cut
|
|
91
|
+
must fall inside a measured gap, and it must fall at the catalogue duration from
|
|
92
|
+
the previous cut. Where they disagree, the cut is clamped and the disagreement
|
|
93
|
+
is reported rather than absorbed.
|
|
94
|
+
|
|
95
|
+
**Cuts** each track with its own start and end, so the groove between tracks is
|
|
96
|
+
discarded instead of being attached to whichever neighbour is nearer.
|
|
97
|
+
|
|
98
|
+
**Verifies by ear**, which is the part that matters. Automatic detection finds
|
|
99
|
+
where music *dominates*; only a person finds where music *is*. So the tool does
|
|
100
|
+
not ask you to scrub through a track and judge. It mixes a 1400 Hz tick at the
|
|
101
|
+
exact cut instant and asks one question: does the tick land in the gap, or on
|
|
102
|
+
the music?
|
|
103
|
+
|
|
104
|
+
## What it is not
|
|
105
|
+
|
|
106
|
+
- Not a library manager. It writes tagged files into a directory and stops.
|
|
107
|
+
Whatever you use to serve or sync your music watches that directory; RipDoctor
|
|
108
|
+
never calls it.
|
|
109
|
+
- Not a restoration tool. No click removal, no declicking, no noise reduction.
|
|
110
|
+
- Not a cataloguing app.
|
|
111
|
+
- Not fully automatic, and not trying to be. The last judgement is yours.
|
|
112
|
+
|
|
113
|
+
## Install
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
pip install ripdoctor
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
It pulls in [beets](https://beets.io), which does the tagging, the path
|
|
120
|
+
formatting and the album-mode ReplayGain a finished record is filed by. Nothing
|
|
121
|
+
else comes with it.
|
|
122
|
+
|
|
123
|
+
beets keeps its own configuration, and RipDoctor uses whatever it finds -
|
|
124
|
+
`BEETSDIR` and the default location are beets' own, and a service that sets one
|
|
125
|
+
passes it down like any other environment variable. Point it at the
|
|
126
|
+
configuration the rest of the machine uses: beets with no configuration is
|
|
127
|
+
beets with no plugins, which means no cover art and no ReplayGain, filing into
|
|
128
|
+
a library database nothing else reads. `ripdoctor doctor` reports when beets
|
|
129
|
+
would file a record somewhere other than the configured library. The single thing RipDoctor
|
|
130
|
+
adds is a layer that turns the import prompt back on, because under `quiet: yes`
|
|
131
|
+
beets never asks: it applies anything above the match threshold and skips
|
|
132
|
+
anything below it without a word, and there is then no such thing as a preview.
|
|
133
|
+
|
|
134
|
+
The system tools it needs are `ffmpeg`, `ffprobe`, `flac` and `metaflac`, plus
|
|
135
|
+
`arecord` if you want to record. `ripdoctor doctor` reports which of those are
|
|
136
|
+
missing and what to install for each.
|
|
137
|
+
|
|
138
|
+
## Commands
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
ripdoctor doctor what this machine has, what it lacks, and what to do
|
|
142
|
+
ripdoctor config every resolved setting, including where the audio lives
|
|
143
|
+
ripdoctor devices capture devices this machine has, and what each accepts
|
|
144
|
+
ripdoctor probe record twenty seconds and say what arrived
|
|
145
|
+
ripdoctor measure compare this signal chain against the shipped thresholds
|
|
146
|
+
ripdoctor record record one side, metered, stopping itself at the run-out
|
|
147
|
+
ripdoctor serve run the web interface
|
|
148
|
+
ripdoctor salvage finish a capture an interrupted session left behind
|
|
149
|
+
ripdoctor name say what a record is, for one ripped before it was
|
|
150
|
+
ripdoctor lookup find a release in the catalogue, usable entries first
|
|
151
|
+
ripdoctor fit place track boundaries from a spec and measured levels
|
|
152
|
+
ripdoctor split cut a plan into tracks
|
|
153
|
+
ripdoctor check build tick clips, one per boundary, for listening to
|
|
154
|
+
ripdoctor import tag the cut tracks and place them in the library
|
|
155
|
+
ripdoctor archive confirm a record arrived before the raw sides are cleared
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`ripdoctor probe` before a record, not after. It turns "drop the needle, wait
|
|
159
|
+
twenty minutes, find out it was the wrong input" into a twenty-second question,
|
|
160
|
+
and tells the four states apart: music, signal with nothing musical in it, an
|
|
161
|
+
empty room, or something merely quiet.
|
|
162
|
+
|
|
163
|
+
`ripdoctor doctor` is the one to run first. It never needs a working machine -
|
|
164
|
+
that is the point of it - and every problem it reports comes with what to do
|
|
165
|
+
about it.
|
|
166
|
+
|
|
167
|
+
The raw sides are never cleared until the record is provably somewhere else -
|
|
168
|
+
`ripdoctor archive` counts what actually arrived and says so, asking beets where
|
|
169
|
+
it filed things rather than guessing at a path.
|
|
170
|
+
|
|
171
|
+
Nothing is cut until a plan validates. A track that would end before it starts,
|
|
172
|
+
overlap its neighbour, collide on a track number or run past the end of the side
|
|
173
|
+
is refused with the reason, rather than written as an empty or duplicated file.
|
|
174
|
+
|
|
175
|
+
## The web interface
|
|
176
|
+
|
|
177
|
+
`ripdoctor serve` is the same pipeline with a waveform in front of it, on the
|
|
178
|
+
machine that holds the library. [`packaging/ripdoctor.service`](packaging/ripdoctor.service)
|
|
179
|
+
runs it as a service; the paths in it are examples and the comments say what
|
|
180
|
+
each one is for. It binds to loopback unless told otherwise, and
|
|
181
|
+
asks for a login either way: it can write to the pool and run ffmpeg, so a
|
|
182
|
+
password is the compensating control for being reachable at all.
|
|
183
|
+
|
|
184
|
+
What the page adds over the commands is the part that needs eyes and ears. The
|
|
185
|
+
1-3 kHz gap curve is drawn under the waveform with the detection threshold on
|
|
186
|
+
it, so a boundary is placed against the measurement rather than against a
|
|
187
|
+
number in a table. Ghost markers show where the catalogue says each cut should
|
|
188
|
+
fall, and the distance between a ghost and a real one is the disagreement the
|
|
189
|
+
fitter reported. Every boundary can be auditioned as a tick clip without
|
|
190
|
+
leaving the page.
|
|
191
|
+
|
|
192
|
+
Recording runs on the server rather than in the browser, so the meter and the
|
|
193
|
+
auto-stop survive a closed laptop. You can listen to the input while cueing the
|
|
194
|
+
needle, which is the only way to tell that the arm is tracking before twenty
|
|
195
|
+
minutes have gone by.
|
|
196
|
+
|
|
197
|
+

|
|
199
|
+
|
|
200
|
+
The bar under the controls is what you watch while cueing. Arm-up handling
|
|
201
|
+
rumble is loud full-band and dead in 1-3 kHz, a silent groove is quiet in both,
|
|
202
|
+
and music is loud in both - so where the band bar sits says which of those is
|
|
203
|
+
happening, before twenty minutes have gone by. Drop the needle when it reads
|
|
204
|
+
`groove`, not below it.
|
|
205
|
+
|
|
206
|
+
## Phases
|
|
207
|
+
|
|
208
|
+
| Phase | State |
|
|
209
|
+
|---|---|
|
|
210
|
+
| 0 — scaffold, decisions, budgets, architecture tests | done |
|
|
211
|
+
| 1 — the detection and fitting core, and its tests | done |
|
|
212
|
+
| 2 — audio seam, config, doctor; offline CLI | done |
|
|
213
|
+
| 3 — capture, the live meter, auto-stop | done, and run against a turntable |
|
|
214
|
+
| 4 — web interface | done |
|
|
215
|
+
| 5 — tagging and import | done |
|
|
216
|
+
| 6 — documentation revision | done |
|
|
217
|
+
| 7 — validate on the server, then cut over | done; cut over 2026-09-07 |
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
make check # lint, types, tests, budgets
|
|
223
|
+
make test
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The core layer computes over decibel envelopes and touches nothing — no
|
|
227
|
+
subprocess, no filesystem, no clock — so the whole algorithm is testable with
|
|
228
|
+
no ffmpeg, no sound card and no audio files. That boundary is enforced by
|
|
229
|
+
`tests/test_architecture.py`, not by convention.
|
|
230
|
+
|
|
231
|
+
On Python 3.14 specifically, this suite is occasionally unreliable through no
|
|
232
|
+
fault of its own - see ADR-019. A failure whose error is *impossible* (an
|
|
233
|
+
unknown opcode, a NameError for something imported at the top of the file) is
|
|
234
|
+
the interpreter, not the code. Run it again. CI uses 3.11 to 3.13 and is not
|
|
235
|
+
affected.
|
|
236
|
+
|
|
237
|
+
`main` is protected by a ruleset kept in `.github/rulesets/main.json`: linear
|
|
238
|
+
history, no force-push, no deletion, and a pull request whose checks have all
|
|
239
|
+
passed. Nobody bypasses it, the author included - a gate with an exception for
|
|
240
|
+
the person most likely to be in a hurry is not a gate.
|
|
241
|
+
|
|
242
|
+
Size budgets are failing checks. The comment ratio is hard and is never raised;
|
|
243
|
+
the others are the author's call. Never write less code to fit a number.
|
|
244
|
+
|
|
245
|
+
The documentation was written ahead of the code and revised once the code was
|
|
246
|
+
complete. What holds it true from here is a set of tests rather than a ratio:
|
|
247
|
+
every command the README names is a real subparser and every subparser is
|
|
248
|
+
named; every threshold appears in `docs/method.md` with the number the code
|
|
249
|
+
actually uses; every element the front end reaches for exists in the markup;
|
|
250
|
+
and every endpoint it calls is a route. See ADR-023 and ADR-037.
|
|
251
|
+
|
|
252
|
+
## Prior work
|
|
253
|
+
|
|
254
|
+
[VinylFlow](https://github.com/olimic1000/vinylflow) (MIT) covers adjacent
|
|
255
|
+
ground and is worth your attention if RipDoctor is not what you want. It takes
|
|
256
|
+
a recording you already made and splits, tags and exports it, with an
|
|
257
|
+
interactive waveform editor for adjusting boundaries.
|
|
258
|
+
|
|
259
|
+
The differences are in scope rather than quality. VinylFlow does not record, so
|
|
260
|
+
capture happens elsewhere and the file is uploaded to it; RipDoctor records on
|
|
261
|
+
the machine that holds the library, so the file is written once and never moves.
|
|
262
|
+
VinylFlow previews the resulting track; RipDoctor is built to judge the boundary
|
|
263
|
+
itself, which is a different question. VinylFlow splits on full-band silence
|
|
264
|
+
detection with a tunable threshold, which is the approach this project exists
|
|
265
|
+
because of.
|
|
266
|
+
|
|
267
|
+
## Licence
|
|
268
|
+
|
|
269
|
+
MIT. See [LICENSE](LICENSE).
|