backpack-backcrack 0.2.0__tar.gz → 0.3.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.
Files changed (38) hide show
  1. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/PKG-INFO +41 -21
  2. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/README.md +39 -19
  3. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/common.py +6 -2
  4. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/config.py +9 -33
  5. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/deps.py +3 -5
  6. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/disc.py +45 -51
  7. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/encode.py +1 -1
  8. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/pattern.py +7 -3
  9. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/rip.py +6 -1
  10. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/ripd.py +6 -3
  11. backpack_backcrack-0.3.0/backcrack/settings.py +239 -0
  12. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/sort.py +1 -1
  13. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/status.py +1 -1
  14. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/swapd.py +1 -1
  15. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/titles.py +1 -1
  16. backpack_backcrack-0.3.0/backcrack/watch.py +360 -0
  17. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backpack_backcrack.egg-info/PKG-INFO +41 -21
  18. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backpack_backcrack.egg-info/SOURCES.txt +4 -1
  19. backpack_backcrack-0.3.0/backpack_backcrack.egg-info/requires.txt +1 -0
  20. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/pyproject.toml +2 -2
  21. backpack_backcrack-0.3.0/tests/test_disc_toc.py +71 -0
  22. backpack_backcrack-0.3.0/tests/test_settings.py +57 -0
  23. backpack_backcrack-0.3.0/tests/test_watch_layout.py +78 -0
  24. backpack_backcrack-0.2.0/backcrack/watch.py +0 -418
  25. backpack_backcrack-0.2.0/backpack_backcrack.egg-info/requires.txt +0 -1
  26. backpack_backcrack-0.2.0/tests/test_disc_toc.py +0 -51
  27. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/LICENSE +0 -0
  28. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/__init__.py +0 -0
  29. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/__main__.py +0 -0
  30. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/cli.py +0 -0
  31. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/diskspeed.py +0 -0
  32. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/encd.py +0 -0
  33. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/namer.py +0 -0
  34. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backcrack/sortd.py +0 -0
  35. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backpack_backcrack.egg-info/dependency_links.txt +0 -0
  36. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backpack_backcrack.egg-info/entry_points.txt +0 -0
  37. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/backpack_backcrack.egg-info/top_level.txt +0 -0
  38. {backpack_backcrack-0.2.0 → backpack_backcrack-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backpack-backcrack
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CD/DVD/Blu-ray rip pipeline - MakeMKV/HandBrake for video, cdparanoia for audio, dynamic %token% library patterning.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/RedEraRrow/backcrack
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
14
14
  Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: backpack-backbone<0.3,>=0.2
17
+ Requires-Dist: backpack-backbone<0.4,>=0.3.2
18
18
  Dynamic: license-file
19
19
 
20
20
  # backcrack
@@ -29,15 +29,27 @@ Python, stdlib only apart from `backbone`, the library the back* tools share
29
29
 
30
30
  ## Install
31
31
 
32
- macOS only (it drives the drives through diskutil and drutil):
32
+ macOS only (it drives the drives through diskutil and drutil). Starting from
33
+ nothing:
34
+
35
+ # Homebrew, if you don't have it yet (it asks for your password)
36
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
37
+ # put brew on your PATH, now and in new windows (Apple Silicon; does
38
+ # nothing on an Intel Mac)
39
+ echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
40
+ eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
33
41
 
34
42
  brew install pipx
43
+ pipx ensurepath # then open a new terminal window
35
44
  pipx install backpack-backcrack
36
45
  backcrack doctor
37
46
 
47
+ macOS's own `python3` is too old (3.9); pipx from Homebrew brings a current
48
+ one. `pipx ensurepath` puts `backcrack` on your PATH for new terminal windows.
49
+
38
50
  `backcrack doctor` lists the rippers and encoders it drives, found or how to
39
51
  install each. For everything: `brew install --cask makemkv` and
40
- `brew install handbrake cdparanoia flac cd-discid`. Paths it can't find
52
+ `brew install handbrake libcdio-paranoia flac`. Paths it can't find
41
53
  itself go in `settings.env` (`MKVCON`, `HBCLI`, …).
42
54
 
43
55
  Every tool is a subcommand of the one command, `backcrack` (`backcrack
@@ -87,15 +99,21 @@ Check progress any time:
87
99
  backcrack watch # live view
88
100
 
89
101
  In `watch`, `q` quits (and offers to stop ripd, encd and sortd with it) and
90
- `s` opens the settings screen - see [Settings](#settings). Both keys, and those of every
91
- list, can be changed under Key bindings on that screen.
102
+ `s` opens the settings screen - see [Settings](#settings). `?` shows or hides
103
+ the keys along the bottom. The keys, and those of every list, can be changed
104
+ under Key bindings on that screen.
105
+
106
+ Each part of `watch` is in its own box: the overview, what's ripping, what's
107
+ encoding, and the latest rip events. In a short window the boxes that matter
108
+ least give up their room first (Recent, then the overview, then Encoding);
109
+ too small for any box, it asks for a bigger window.
92
110
 
93
111
  ## What it detects
94
112
 
95
113
  - **DVD / Blu-ray** - MakeMKV rips each title to a lossless MKV, HandBrake
96
114
  encodes the ones matching a configured duration class - see
97
115
  `DURATION_CLASSES` below.
98
- - **Audio CD** - cdparanoia extracts each track losslessly
116
+ - **Audio CD** - cd-paranoia extracts each track losslessly
99
117
  (`track01.cdda.wav`...), then each is compressed to `AUDIO_FORMAT` (flac
100
118
  by default) as `track01.flac`...
101
119
 
@@ -104,8 +122,8 @@ Both land in the same shape: `source/` (lossless rip) and `encoded/`
104
122
  than the main window.
105
123
 
106
124
  A video disc is known by its volume label. An audio CD has no useful label,
107
- so it is named `AudioCD-<n>tracks-<id>`, where the id is cd-discid's disc id
108
- (or, without cd-discid, a hash of cdparanoia's track table). That name is
125
+ so it is named `AudioCD-<n>tracks-<id>`, where the id is the freedb disc id worked
126
+ out from its track table. That name is
109
127
  what shows in the log and in `UNSORTED/`, and what a `labels.map` line for
110
128
  the CD has to use.
111
129
 
@@ -184,7 +202,7 @@ you see combing, set `DEINTERLACE_ARGS` to HandBrake's deinterlace flags,
184
202
  e.g. `DEINTERLACE_ARGS=--comb-detect --decomb`. Leave it empty for
185
203
  progressive sources.
186
204
 
187
- For audio, `cdparanoia -Q -d <device>` lists a disc's tracks directly if you
205
+ For audio, `cd-paranoia -Q -d <device>` lists a disc's tracks directly if you
188
206
  want to sanity-check before ripping.
189
207
 
190
208
  ## Encoding throughput
@@ -251,17 +269,20 @@ Each setting is read, in order, from an environment variable, then
251
269
  `backcrack/config.py`. So a one-off `LIBRARY=... ripd` overrides
252
270
  everything, and `settings.env` holds what you want every time.
253
271
 
254
- `s` in `watch` opens a settings screen over most of them and writes your
255
- changes to `settings.env`. The tool paths below aren't on it; set those in
272
+ `s` in `watch` opens the Settings screen: most of them, in sections, each
273
+ with its value beside it. Enter changes one (space flips an on/off one), and
274
+ it's saved to `settings.env` at once. A number or a duration class that
275
+ can't be read isn't taken. The tool paths below aren't on it; set those in
256
276
  `settings.env` or the environment. The daemons read their settings at
257
- startup, so restart `ripd`, `encd` or `sortd` to pick a change up.
277
+ startup, so after a change the screen says which running ones to restart.
258
278
 
259
279
  The ones not covered elsewhere in this README:
260
280
 
261
281
  | Setting | Default | What it does |
262
282
  |---|---|---|
263
- | `MKVCON`, `HBCLI`, `CDPARANOIA`, `CD_DISCID`, `FLAC`, `FFMPEG` | found on PATH (MakeMKV at `/Applications/MakeMKV.app`) | tool paths |
283
+ | `MKVCON`, `HBCLI`, `CDPARANOIA`, `FLAC`, `FFMPEG` | found on PATH (MakeMKV at `/Applications/MakeMKV.app`) | tool paths |
264
284
  | `NOTIFY_ENCODES` | `0` | also push when a disc finishes encoding |
285
+ | `DEBUG` | `0` | the diagnostics log, `backcrack.log` in your config directory: what failed quietly (a MusicBrainz lookup, a HandBrake scan, a crashed rip) and why |
265
286
  | `ACCENT` | `green` | the accent colour: `green`, `red`, `yellow`, `blue`, `magenta`, `cyan` (your terminal's own), `amber`, `coral`, `rose`, `lavender`, `sky`, `mint`, or `#RRGGBB` |
266
287
  | `WATCH_INTERVAL` | `1` | seconds between `watch` refreshes |
267
288
  | `SORT_INTERVAL` | `5` | seconds between `sortd` passes |
@@ -293,7 +314,7 @@ The directory is `$BACKCRACK_CONFIG_DIR` if set, else
293
314
  first run. Point `BACKCRACK_CONFIG_DIR` somewhere else to keep separate sets
294
315
  of overrides for separate shelves.
295
316
 
296
- The same screen has an entry for adding a `labels.map` line, which is the
317
+ The same screen has Add a labels.map line…, which is the
297
318
  manual escape hatch for a disc whose %tokens% couldn't be resolved - pick it
298
319
  out of `UNSORTED/`, fill in the tokens the active pattern needs, and `sortd`
299
320
  files it within a few seconds.
@@ -339,16 +360,15 @@ names). With another pattern or other folder names, don't use it.
339
360
 
340
361
  - **MakeMKV** (`makemkvcon`) - for DVD/Blu-ray. `brew install --cask makemkv`.
341
362
  - **HandBrakeCLI** - to encode video. `brew install handbrake`.
342
- - **cdparanoia** - for audio CDs. `brew install cdparanoia`.
363
+ - **cd-paranoia** - for audio CDs: ripping, and the track table that names
364
+ each CD and looks up its artist and album. `brew install libcdio-paranoia`
365
+ (or `cdparanoia` from your Linux package manager).
343
366
  - **flac** (or ffmpeg) - to compress ripped audio. `brew install flac`.
344
367
  With neither, tracks are copied to `encoded/` as `.wav`.
345
- - **cd-discid** - optional, for automatic artist/album lookup on audio CDs.
346
- `brew install cd-discid`. Without it, every CD goes to `UNSORTED/` until
347
- you add a `labels.map` line.
348
368
 
349
369
  `backcrack doctor` checks all of them. A missing tool also gets a warning at
350
- startup rather than a silent failure: `ripd` warns about makemkvcon,
351
- cdparanoia and cd-discid, and `encd` about HandBrakeCLI and flac/ffmpeg. The
370
+ startup rather than a silent failure: `ripd` warns about makemkvcon and
371
+ cd-paranoia, and `encd` about HandBrakeCLI and flac/ffmpeg. The
352
372
  daemon still runs; that disc kind just won't rip or encode until the tool is
353
373
  installed.
354
374
 
@@ -10,15 +10,27 @@ Python, stdlib only apart from `backbone`, the library the back* tools share
10
10
 
11
11
  ## Install
12
12
 
13
- macOS only (it drives the drives through diskutil and drutil):
13
+ macOS only (it drives the drives through diskutil and drutil). Starting from
14
+ nothing:
15
+
16
+ # Homebrew, if you don't have it yet (it asks for your password)
17
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
18
+ # put brew on your PATH, now and in new windows (Apple Silicon; does
19
+ # nothing on an Intel Mac)
20
+ echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
21
+ eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
14
22
 
15
23
  brew install pipx
24
+ pipx ensurepath # then open a new terminal window
16
25
  pipx install backpack-backcrack
17
26
  backcrack doctor
18
27
 
28
+ macOS's own `python3` is too old (3.9); pipx from Homebrew brings a current
29
+ one. `pipx ensurepath` puts `backcrack` on your PATH for new terminal windows.
30
+
19
31
  `backcrack doctor` lists the rippers and encoders it drives, found or how to
20
32
  install each. For everything: `brew install --cask makemkv` and
21
- `brew install handbrake cdparanoia flac cd-discid`. Paths it can't find
33
+ `brew install handbrake libcdio-paranoia flac`. Paths it can't find
22
34
  itself go in `settings.env` (`MKVCON`, `HBCLI`, …).
23
35
 
24
36
  Every tool is a subcommand of the one command, `backcrack` (`backcrack
@@ -68,15 +80,21 @@ Check progress any time:
68
80
  backcrack watch # live view
69
81
 
70
82
  In `watch`, `q` quits (and offers to stop ripd, encd and sortd with it) and
71
- `s` opens the settings screen - see [Settings](#settings). Both keys, and those of every
72
- list, can be changed under Key bindings on that screen.
83
+ `s` opens the settings screen - see [Settings](#settings). `?` shows or hides
84
+ the keys along the bottom. The keys, and those of every list, can be changed
85
+ under Key bindings on that screen.
86
+
87
+ Each part of `watch` is in its own box: the overview, what's ripping, what's
88
+ encoding, and the latest rip events. In a short window the boxes that matter
89
+ least give up their room first (Recent, then the overview, then Encoding);
90
+ too small for any box, it asks for a bigger window.
73
91
 
74
92
  ## What it detects
75
93
 
76
94
  - **DVD / Blu-ray** - MakeMKV rips each title to a lossless MKV, HandBrake
77
95
  encodes the ones matching a configured duration class - see
78
96
  `DURATION_CLASSES` below.
79
- - **Audio CD** - cdparanoia extracts each track losslessly
97
+ - **Audio CD** - cd-paranoia extracts each track losslessly
80
98
  (`track01.cdda.wav`...), then each is compressed to `AUDIO_FORMAT` (flac
81
99
  by default) as `track01.flac`...
82
100
 
@@ -85,8 +103,8 @@ Both land in the same shape: `source/` (lossless rip) and `encoded/`
85
103
  than the main window.
86
104
 
87
105
  A video disc is known by its volume label. An audio CD has no useful label,
88
- so it is named `AudioCD-<n>tracks-<id>`, where the id is cd-discid's disc id
89
- (or, without cd-discid, a hash of cdparanoia's track table). That name is
106
+ so it is named `AudioCD-<n>tracks-<id>`, where the id is the freedb disc id worked
107
+ out from its track table. That name is
90
108
  what shows in the log and in `UNSORTED/`, and what a `labels.map` line for
91
109
  the CD has to use.
92
110
 
@@ -165,7 +183,7 @@ you see combing, set `DEINTERLACE_ARGS` to HandBrake's deinterlace flags,
165
183
  e.g. `DEINTERLACE_ARGS=--comb-detect --decomb`. Leave it empty for
166
184
  progressive sources.
167
185
 
168
- For audio, `cdparanoia -Q -d <device>` lists a disc's tracks directly if you
186
+ For audio, `cd-paranoia -Q -d <device>` lists a disc's tracks directly if you
169
187
  want to sanity-check before ripping.
170
188
 
171
189
  ## Encoding throughput
@@ -232,17 +250,20 @@ Each setting is read, in order, from an environment variable, then
232
250
  `backcrack/config.py`. So a one-off `LIBRARY=... ripd` overrides
233
251
  everything, and `settings.env` holds what you want every time.
234
252
 
235
- `s` in `watch` opens a settings screen over most of them and writes your
236
- changes to `settings.env`. The tool paths below aren't on it; set those in
253
+ `s` in `watch` opens the Settings screen: most of them, in sections, each
254
+ with its value beside it. Enter changes one (space flips an on/off one), and
255
+ it's saved to `settings.env` at once. A number or a duration class that
256
+ can't be read isn't taken. The tool paths below aren't on it; set those in
237
257
  `settings.env` or the environment. The daemons read their settings at
238
- startup, so restart `ripd`, `encd` or `sortd` to pick a change up.
258
+ startup, so after a change the screen says which running ones to restart.
239
259
 
240
260
  The ones not covered elsewhere in this README:
241
261
 
242
262
  | Setting | Default | What it does |
243
263
  |---|---|---|
244
- | `MKVCON`, `HBCLI`, `CDPARANOIA`, `CD_DISCID`, `FLAC`, `FFMPEG` | found on PATH (MakeMKV at `/Applications/MakeMKV.app`) | tool paths |
264
+ | `MKVCON`, `HBCLI`, `CDPARANOIA`, `FLAC`, `FFMPEG` | found on PATH (MakeMKV at `/Applications/MakeMKV.app`) | tool paths |
245
265
  | `NOTIFY_ENCODES` | `0` | also push when a disc finishes encoding |
266
+ | `DEBUG` | `0` | the diagnostics log, `backcrack.log` in your config directory: what failed quietly (a MusicBrainz lookup, a HandBrake scan, a crashed rip) and why |
246
267
  | `ACCENT` | `green` | the accent colour: `green`, `red`, `yellow`, `blue`, `magenta`, `cyan` (your terminal's own), `amber`, `coral`, `rose`, `lavender`, `sky`, `mint`, or `#RRGGBB` |
247
268
  | `WATCH_INTERVAL` | `1` | seconds between `watch` refreshes |
248
269
  | `SORT_INTERVAL` | `5` | seconds between `sortd` passes |
@@ -274,7 +295,7 @@ The directory is `$BACKCRACK_CONFIG_DIR` if set, else
274
295
  first run. Point `BACKCRACK_CONFIG_DIR` somewhere else to keep separate sets
275
296
  of overrides for separate shelves.
276
297
 
277
- The same screen has an entry for adding a `labels.map` line, which is the
298
+ The same screen has Add a labels.map line…, which is the
278
299
  manual escape hatch for a disc whose %tokens% couldn't be resolved - pick it
279
300
  out of `UNSORTED/`, fill in the tokens the active pattern needs, and `sortd`
280
301
  files it within a few seconds.
@@ -320,16 +341,15 @@ names). With another pattern or other folder names, don't use it.
320
341
 
321
342
  - **MakeMKV** (`makemkvcon`) - for DVD/Blu-ray. `brew install --cask makemkv`.
322
343
  - **HandBrakeCLI** - to encode video. `brew install handbrake`.
323
- - **cdparanoia** - for audio CDs. `brew install cdparanoia`.
344
+ - **cd-paranoia** - for audio CDs: ripping, and the track table that names
345
+ each CD and looks up its artist and album. `brew install libcdio-paranoia`
346
+ (or `cdparanoia` from your Linux package manager).
324
347
  - **flac** (or ffmpeg) - to compress ripped audio. `brew install flac`.
325
348
  With neither, tracks are copied to `encoded/` as `.wav`.
326
- - **cd-discid** - optional, for automatic artist/album lookup on audio CDs.
327
- `brew install cd-discid`. Without it, every CD goes to `UNSORTED/` until
328
- you add a `labels.map` line.
329
349
 
330
350
  `backcrack doctor` checks all of them. A missing tool also gets a warning at
331
- startup rather than a silent failure: `ripd` warns about makemkvcon,
332
- cdparanoia and cd-discid, and `encd` about HandBrakeCLI and flac/ffmpeg. The
351
+ startup rather than a silent failure: `ripd` warns about makemkvcon and
352
+ cd-paranoia, and `encd` about HandBrakeCLI and flac/ffmpeg. The
333
353
  daemon still runs; that disc kind just won't rip or encode until the tool is
334
354
  installed.
335
355
 
@@ -3,6 +3,7 @@ import json
3
3
  import subprocess
4
4
 
5
5
  from backbone.files import log_line
6
+ from backbone.log import log as diag
6
7
  from backbone.notify import ntfy
7
8
  from backbone.procs import find_processes, spawn_module, stop_processes
8
9
 
@@ -71,15 +72,18 @@ def hb_json(input_path: str) -> dict:
71
72
  [cfg.HBCLI, "-i", input_path, "--title", "0", "--scan", "--json"],
72
73
  capture_output=True, text=True, timeout=120,
73
74
  ).stdout
74
- except (OSError, subprocess.TimeoutExpired):
75
+ except (OSError, subprocess.TimeoutExpired) as e:
76
+ diag.warning("HandBrake scan of %s failed: %s", input_path, e)
75
77
  return {}
76
78
  marker = "JSON Title Set:"
77
79
  idx = out.find(marker)
78
80
  if idx < 0:
81
+ diag.warning("HandBrake scan of %s gave no title set", input_path)
79
82
  return {}
80
83
  try:
81
84
  return json.loads(out[idx + len(marker):])
82
- except json.JSONDecodeError:
85
+ except json.JSONDecodeError as e:
86
+ diag.warning("HandBrake scan of %s: unreadable JSON: %s", input_path, e)
83
87
  return {}
84
88
 
85
89
 
@@ -26,8 +26,8 @@ def _default_config_dir() -> Path:
26
26
  CONFIG_DIR = Path(os.getenv("BACKCRACK_CONFIG_DIR") or _default_config_dir())
27
27
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
28
28
 
29
- # backbone keeps the key bindings, the hints switch and its log here too.
30
- from backbone import app as _app # noqa: E402
29
+ # backbone keeps the key bindings, the hints switch and its diagnostics log here too.
30
+ from backbone import app as _app, log as _log # noqa: E402
31
31
  _app.configure("backcrack", CONFIG_DIR)
32
32
 
33
33
  # Settings changed from `backcrack watch`'s settings screen land here rather
@@ -65,7 +65,7 @@ def save_setting(name: str, value: str) -> None:
65
65
  """Persist one setting to SETTINGS_FILE, replacing its line if already
66
66
  present. Only processes started afterwards see it (ripd/encd need
67
67
  restarting); a caller that wants it in its own running `cfg` has to set
68
- that too (see watch.py).
68
+ that too (see settings.py).
69
69
  """
70
70
  lines = SETTINGS_FILE.read_text().splitlines() if SETTINGS_FILE.exists() else []
71
71
  out, found = [], False
@@ -125,8 +125,7 @@ LIBRARY = Path(_env("LIBRARY", str(Path.home() / "Media" / "rips"))).expanduser(
125
125
  MKVCON = _env("MKVCON", _env("MKV", shutil.which("makemkvcon")
126
126
  or "/Applications/MakeMKV.app/Contents/MacOS/makemkvcon"))
127
127
  HBCLI = _env("HBCLI", shutil.which("HandBrakeCLI") or "/opt/homebrew/bin/HandBrakeCLI")
128
- CDPARANOIA = _env("CDPARANOIA", shutil.which("cdparanoia") or "")
129
- CD_DISCID = _env("CD_DISCID", shutil.which("cd-discid") or "")
128
+ CDPARANOIA = _env("CDPARANOIA", shutil.which("cd-paranoia") or shutil.which("cdparanoia") or "")
130
129
  FLAC = _env("FLAC", shutil.which("flac") or "")
131
130
  FFMPEG = _env("FFMPEG", shutil.which("ffmpeg") or "")
132
131
 
@@ -224,39 +223,16 @@ PATTERN_AUDIO = _env("PATTERN_AUDIO", "%artist%/%album%")
224
223
  # on. A failed disc is deliberately left in the drive so it retries itself.
225
224
  MAX_RETRIES = _env_int("MAX_RETRIES", 3)
226
225
 
227
- # ---- settings exposed to `backcrack watch`'s settings screen -------------
228
- # (env var, type, one-line label). Add a tuple here for any setting above
229
- # that should be editable there - type is "str", "int", "bool", or "path".
230
- # Nothing else in the pipeline reads this list; it's UI metadata only.
231
- SETTINGS = [
232
- ("LIBRARY", "path", "Library root"),
233
- ("RIP_MODE", "str", "Rip mode (titles / video_ts)"),
234
- ("DURATION_CLASSES", "str", "Duration classes (name:min-max:folder:quality[:dedup],...)"),
235
- ("ENCODE_JOBS", "int", "Concurrent encode jobs"),
236
- ("VIDEO_ENCODER", "str", "HandBrake video encoder"),
237
- ("ENCODER_PRESET", "str", "HandBrake encoder preset"),
238
- ("ENCODE_THREADS", "int", "Threads per encode job"),
239
- ("DEINTERLACE_ARGS", "str", "Deinterlace args (blank = off)"),
240
- ("AUDIO_FORMAT", "str", "Audio rip format"),
241
- ("PATTERN_VIDEO", "str", "Video destination pattern"),
242
- ("PATTERN_AUDIO", "str", "Audio destination pattern"),
243
- ("MAX_RETRIES", "int", "Max retries before giving up on a disc"),
244
- ("NOTIFY_ENCODES", "bool", "Notify on encode complete"),
245
- ("LAUNCH_RIPD", "bool", "Bare `backcrack` also launches ripd"),
246
- ("LAUNCH_ENCD", "bool", "Bare `backcrack` also launches encd"),
247
- ("LAUNCH_SORTD", "bool", "Bare `backcrack` also launches sortd"),
248
- ("LAUNCH_WATCH", "bool", "Bare `backcrack` also launches watch"),
249
- ("MIN_TITLE_S", "int", "Shortest title MakeMKV rips (seconds)"),
250
- ("NTFY_TOPIC", "str", "ntfy topic (blank = no pushes)"),
251
- ("NTFY_SERVER", "str", "ntfy server"),
252
- ("ACCENT", "accent", "Accent colour (green, red, blue, amber… or #RRGGBB)"),
253
- ]
254
-
255
226
  # The accent colour of every screen (backbone.ui.ACCENT_PRESETS key or
256
227
  # "#RRGGBB"), applied here once so each tool gets it by importing config.
257
228
  ACCENT = _env("ACCENT", "green")
258
229
  _ui.set_accent(ACCENT)
259
230
 
231
+ # backbone's diagnostics log, CONFIG_DIR/backcrack.log: what failed quietly
232
+ # (a lookup, a scan, a crashed rip thread) and why. Every process writes it.
233
+ DEBUG = _env("DEBUG", "0") == "1"
234
+ _log.configure(DEBUG)
235
+
260
236
  # Derived. Don't edit.
261
237
  # Your files, in CONFIG_DIR (see the top of this file): manual disc->token
262
238
  # overrides, and the per-season episode titles namer.py renames from.
@@ -21,12 +21,10 @@ def deps() -> list[Dep]:
21
21
  Dep("HandBrake", _path(cfg.HBCLI), "encoding video", hints={
22
22
  "macos": "brew install handbrake", "debian": "sudo apt install handbrake-cli",
23
23
  "other": "https://handbrake.fr/downloads2.php"}),
24
- Dep("cdparanoia", _path(cfg.CDPARANOIA), "ripping audio CDs", hints={
25
- "macos": "brew install cdparanoia", "debian": "sudo apt install cdparanoia"}),
24
+ Dep("cd-paranoia", _path(cfg.CDPARANOIA), "ripping and looking up audio CDs", hints={
25
+ "macos": "brew install libcdio-paranoia", "debian": "sudo apt install cdparanoia"}),
26
26
  Dep("flac", _path(cfg.FLAC), "encoding CD audio", hints={
27
27
  "macos": "brew install flac", "debian": "sudo apt install flac"}),
28
- Dep("cd-discid", _path(cfg.CD_DISCID), "looking up CDs", hints={
29
- "macos": "brew install cd-discid", "debian": "sudo apt install cd-discid"}),
30
28
  Dep("ffmpeg", _path(cfg.FFMPEG), "audio formats other than FLAC", hints={
31
29
  "macos": "brew install ffmpeg", "debian": "sudo apt install ffmpeg"}),
32
30
  ]
@@ -36,4 +34,4 @@ def doctor() -> None:
36
34
  """backcrack doctor: every tool it drives, found or how to install it."""
37
35
  from backbone.deps import report_lines
38
36
  print("\n".join(report_lines(deps())))
39
- print("Paths can be set in settings.env (MKVCON, HBCLI, CDPARANOIA, FLAC, CD_DISCID, FFMPEG).")
37
+ print("Paths can be set in settings.env (MKVCON, HBCLI, CDPARANOIA, FLAC, FFMPEG).")
@@ -8,11 +8,14 @@ import subprocess
8
8
  import time
9
9
  from pathlib import Path
10
10
 
11
+ from backbone.log import log as diag
12
+
11
13
  from backcrack import config as cfg
12
14
 
13
15
 
14
- # Optical drive speeds are quoted in multiples of DVD 1x.
16
+ # Optical drive speeds are quoted in multiples of 1x for that kind of disc.
15
17
  DVD_1X_BPS = 1_385_000
18
+ CD_1X_BPS = 176_400
16
19
 
17
20
 
18
21
  def raw(dev: str) -> str:
@@ -68,41 +71,35 @@ def pending_discs():
68
71
  yield dev, "audio", Path(dev).name
69
72
 
70
73
 
71
- def cd_discid(rdev: str) -> list:
72
- """cd-discid's fields for the CD in `rdev` (freedb id, track count,
73
- offsets..., seconds), or [] if it isn't installed or can't read it."""
74
- if not cfg.CD_DISCID:
75
- return []
76
- try:
77
- return subprocess.run([cfg.CD_DISCID, rdev], capture_output=True, text=True, timeout=15).stdout.split()
78
- except (OSError, subprocess.TimeoutExpired):
79
- return []
80
-
81
-
82
74
  def cd_toc(rdev: str):
83
75
  """The CD's table of contents as MusicBrainz wants it: (first track, last
84
- track, lead-out, [track offsets]) in sectors, or None. Uses cd-discid's
85
- exact --musicbrainz output; an older cd-discid only gives the length in
86
- whole seconds, so the lead-out is then approximate (MusicBrainz's toc
87
- lookup still finds near matches)."""
88
- if not cfg.CD_DISCID:
76
+ track, lead-out, [track offsets]) in sectors from the disc's start, or
77
+ None. Read from cd-paranoia's track list, which counts from track 1's
78
+ pregap, hence the 150. Audio tracks only, so on an enhanced CD the
79
+ lead-out is the last audio track's end (MusicBrainz's toc lookup still
80
+ finds the near match)."""
81
+ if not cfg.CDPARANOIA:
89
82
  return None
90
83
  try:
91
- out = subprocess.run([cfg.CD_DISCID, "--musicbrainz", rdev], capture_output=True, text=True, timeout=15).stdout.split()
92
- except (OSError, subprocess.TimeoutExpired):
93
- out = []
94
- nums = [int(x) for x in out if x.isdigit()]
95
- if len(nums) >= 3 and nums[0] == len(nums) - 2: # count, offsets..., lead-out
96
- return 1, nums[0], nums[-1], nums[1:-1]
97
- fields = cd_discid(rdev) # freedb: id, count, offsets..., seconds
98
- if len(fields) < 3 or not fields[1].isdigit():
84
+ out = subprocess.run([cfg.CDPARANOIA, "-Q", "-d", rdev], capture_output=True, text=True, timeout=30).stderr
85
+ except (OSError, subprocess.TimeoutExpired) as e:
86
+ diag.warning("cd-paranoia -Q on %s failed: %s", rdev, e)
99
87
  return None
100
- n = int(fields[1])
101
- try:
102
- offsets = [int(x) for x in fields[2:2 + n]]
103
- return 1, n, int(fields[2 + n]) * 75, offsets
104
- except (ValueError, IndexError):
88
+ # " 1. 16503 [03:40.03] 0 [00:00.00] no no 2": number, length, begin
89
+ tracks = [(int(m[1]), int(m[2]), int(m[3]))
90
+ for m in re.finditer(r"^\s*(\d+)\.\s+(\d+)\s+\[[^]]*\]\s+(\d+)", out, re.M)]
91
+ if not tracks:
92
+ diag.warning("cd-paranoia -Q on %s listed no tracks: %s", rdev, out.strip()[-300:])
105
93
  return None
94
+ first, last = tracks[0][0], tracks[-1][0]
95
+ return first, last, tracks[-1][2] + tracks[-1][1] + 150, [begin + 150 for _, _, begin in tracks]
96
+
97
+
98
+ def freedb_id(toc) -> str:
99
+ """The freedb (CDDB) disc id for a toc, as cd-discid printed it."""
100
+ _, _, leadout, offsets = toc
101
+ digits = sum(sum(map(int, str(o // 75))) for o in offsets)
102
+ return f"{(digits % 255) << 24 | (leadout // 75 - offsets[0] // 75) << 8 | len(offsets):08x}"
106
103
 
107
104
 
108
105
  def musicbrainz_discid(toc) -> str:
@@ -116,26 +113,22 @@ def musicbrainz_discid(toc) -> str:
116
113
  return digest.translate(str.maketrans("+/=", "._-"))
117
114
 
118
115
 
119
- def audio_label(rdev: str):
120
- """A name for the CD in `rdev` that stays the same whichever drive it is
121
- in: "AudioCD-<n>tracks-<id>". The id is cd-discid's, or a hash of
122
- cdparanoia's track table when cd-discid isn't installed. None if
123
- neither can read the disc.
124
- """
125
- fields = cd_discid(rdev)
126
- if len(fields) >= 2 and fields[1].isdigit():
127
- return f"AudioCD-{fields[1]}tracks-{fields[0]}"
128
- if not cfg.CDPARANOIA:
129
- return None
130
- try:
131
- toc = subprocess.run([cfg.CDPARANOIA, "-Q", "-d", rdev], capture_output=True, text=True, timeout=30).stderr
132
- except (OSError, subprocess.TimeoutExpired):
133
- return None
134
- tracks = [line.split() for line in toc.splitlines() if re.match(r"^\s*\d+\.\s+\d+", line)]
135
- if not tracks:
136
- return None
137
- digest = hashlib.sha1(" ".join(t[1] for t in tracks).encode()).hexdigest()[:8]
138
- return f"AudioCD-{len(tracks)}tracks-{digest}"
116
+ def audio_label(toc) -> str:
117
+ """A name for a CD that stays the same whichever drive it is in."""
118
+ return f"AudioCD-{len(toc[3])}tracks-{freedb_id(toc)}"
119
+
120
+
121
+ # Holds audio_bytes() for a rip in progress, in its folder, for watch's
122
+ # progress bar: cd-paranoia runs inside the folder rather than naming it, so
123
+ # watch can't match the rip to its drive the way it does for MakeMKV.
124
+ DISC_BYTES_FILE = ".disc-bytes"
125
+
126
+
127
+ def audio_bytes(toc) -> int:
128
+ """What ripping every track to WAV comes to: 2352 bytes a sector, plus
129
+ each file's 44-byte header."""
130
+ _, _, leadout, offsets = toc
131
+ return (leadout - offsets[0]) * 2352 + 44 * len(offsets)
139
132
 
140
133
 
141
134
  _DRV_RE = re.compile(r'^DRV:(\d+),2,\d+,\d+,"([^"]*)","([^"]*)","([^"]*)"')
@@ -147,7 +140,8 @@ def mkv_drives(timeout: int = 60) -> list:
147
140
  try:
148
141
  out = subprocess.run([cfg.MKVCON, "-r", "--cache=1", "info", "disc:9999"],
149
142
  capture_output=True, text=True, timeout=timeout).stdout
150
- except (OSError, subprocess.TimeoutExpired):
143
+ except (OSError, subprocess.TimeoutExpired) as e:
144
+ diag.warning("makemkvcon drive listing failed: %s", e)
151
145
  return []
152
146
  return [m.groups() for m in map(_DRV_RE.match, out.splitlines()) if m]
153
147
 
@@ -159,7 +159,7 @@ def process_audio_job(label: str, dest: Path) -> None:
159
159
  jobs = []
160
160
  ext = audio_ext()
161
161
  for f in sorted(src.glob("*.wav")):
162
- # cdparanoia -B names tracks track01.cdda.wav
162
+ # cd-paranoia -B names tracks track01.cdda.wav
163
163
  out = dest / "encoded" / f"{f.stem.removesuffix('.cdda')}.{ext}"
164
164
  if out.exists() and out.stat().st_size > 0:
165
165
  n_skip += 1
@@ -7,6 +7,8 @@ import urllib.request
7
7
  from pathlib import Path
8
8
  from typing import Optional
9
9
 
10
+ from backbone.log import log as diag
11
+
10
12
  from backcrack import config as cfg
11
13
  from backcrack import disc
12
14
 
@@ -93,8 +95,8 @@ def auto_audio_tokens(dev: Optional[str]) -> Optional[dict]:
93
95
  toc = disc.cd_toc(dev) if dev else None
94
96
  if not toc:
95
97
  return None
96
- # The MusicBrainz disc id (cd-discid's own id is freedb's, which MusicBrainz
97
- # doesn't know), plus the toc so a near match is found when there's no exact one.
98
+ # The MusicBrainz disc id (the label's freedb id is one MusicBrainz doesn't
99
+ # know), plus the toc so a near match is found when there's no exact one.
98
100
  toc_param = "+".join(str(n) for n in (toc[0], toc[1], toc[2], *toc[3]))
99
101
  try:
100
102
  req = urllib.request.Request(
@@ -104,10 +106,12 @@ def auto_audio_tokens(dev: Optional[str]) -> Optional[dict]:
104
106
  )
105
107
  with urllib.request.urlopen(req, timeout=10) as resp:
106
108
  data = json.loads(resp.read())
107
- except Exception:
109
+ except Exception as e:
110
+ diag.warning("MusicBrainz lookup for %s failed: %s: %s", dev, type(e).__name__, e)
108
111
  return None
109
112
  releases = data.get("releases") or []
110
113
  if not releases:
114
+ diag.info("MusicBrainz has no release for disc id %s", disc.musicbrainz_discid(toc))
111
115
  return None
112
116
  release = releases[0]
113
117
  artist_credit = release.get("artist-credit") or []
@@ -139,6 +139,7 @@ def mark_unsorted_kind(dest: Path, kind: str) -> None:
139
139
 
140
140
 
141
141
  def finish_rip(label: str, dest: Path, cdev: str, ok: bool, mins: int, kind: str) -> None:
142
+ (dest / disc.DISC_BYTES_FILE).unlink(missing_ok=True)
142
143
  kb = dir_size_kb(dest)
143
144
  gb = human_gb(kb)
144
145
  rel = str(dest.relative_to(cfg.LIBRARY))
@@ -243,7 +244,9 @@ def rip_audio_disc(dev: str, label: str) -> None:
243
244
  # is only the device name there, so name the CD itself now this thread
244
245
  # holds the drive.
245
246
  cdev, rdev = dev, disc.raw(dev)
246
- label = disc.audio_label(rdev) or label
247
+ toc = disc.cd_toc(rdev)
248
+ if toc:
249
+ label = disc.audio_label(toc)
247
250
  if already_handled(label, cdev):
248
251
  return
249
252
 
@@ -256,6 +259,8 @@ def rip_audio_disc(dev: str, label: str) -> None:
256
259
  mark_unsorted_kind(dest, "audio")
257
260
 
258
261
  nsrc = dest / "source"; nsrc.mkdir(parents=True, exist_ok=True)
262
+ if toc:
263
+ (dest / disc.DISC_BYTES_FILE).write_text(f"{disc.audio_bytes(toc)}\n")
259
264
  t0 = time.monotonic()
260
265
  rc = subprocess.run(
261
266
  [cfg.CDPARANOIA, "-d", rdev, "-B", "1-"], cwd=nsrc, capture_output=True, timeout=None,
@@ -10,6 +10,8 @@ import os
10
10
  import time
11
11
  from pathlib import Path
12
12
 
13
+ from backbone.log import log as diag
14
+
13
15
  from backcrack import config as cfg
14
16
  from backcrack import disc
15
17
  from backcrack.rip import rip_video_disc, rip_audio_disc
@@ -40,9 +42,7 @@ def main() -> None:
40
42
  if not Path(cfg.MKVCON).exists():
41
43
  print(f"WARNING: makemkvcon not found at {cfg.MKVCON} - video discs will fail. See `backcrack doctor`.")
42
44
  if not cfg.CDPARANOIA:
43
- print("WARNING: cdparanoia not found - audio CDs will fail. See `backcrack doctor`.")
44
- if not cfg.CD_DISCID:
45
- print("WARNING: cd-discid not found - audio CDs will go to UNSORTED/. See `backcrack doctor`.")
45
+ print("WARNING: cd-paranoia not found - audio CDs will fail. See `backcrack doctor`.")
46
46
  if not cfg.NTFY_TOPIC:
47
47
  print("WARNING: NTFY_TOPIC is not set (env var or settings.env) - no pushes will be sent.")
48
48
 
@@ -68,6 +68,9 @@ def main() -> None:
68
68
  def _run(dev=dev, label=label, target=target, lock=lock):
69
69
  try:
70
70
  target(dev, label)
71
+ except Exception:
72
+ diag.exception("rip of %s in %s crashed", label, dev)
73
+ raise
71
74
  finally:
72
75
  lock.unlink(missing_ok=True)
73
76