Fix 6 bugs: shared stdin reader, CDDB multiline, type annotation, crash fixes

- ripper: replace per-call stdin daemon threads with a shared module-level
  reader (_stdin_queue + _read_line), preventing orphan threads from stealing
  stdin input after photo uploads; all 8 input() calls in interactive_rip
  now use _read_line()
- ripper: _stream_abcde return type annotation fixed (2-tuple → 3-tuple)
- ripper: disc retry rejection now breaks gracefully instead of raising
  unhandled RuntimeError that crashed the program
- ripper: int() on disc number input wrapped in try/except
- cddb: multi-line DTITLE/TTITLE values are now concatenated instead of
  only keeping the last line (per CDDB/xmcd protocol spec)
- cli: removed unreachable dead code block in apply command
- scanner_server: upload form auto-resets after 3s for repeated uploads
- tests: _scanner_patches() updated to mock _read_line alongside
  _input_or_scan (225 tests passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-19 19:03:06 +01:00
commit 8c25bc65be
5 changed files with 100 additions and 40 deletions

View file

@ -358,7 +358,7 @@ class TestInteractiveRipEanFirst:
config = RipperConfig(output_dir=tmp_path)
sp = self._scanner_patches()
with (
sp[0], sp[1],
sp[0], sp[1], sp[2],
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
patch("builtins.input", side_effect=iter(inputs)),
patch("musiksammlung.ripper.lookup_by_barcode", return_value=(_MB_ALBUM, "fake-mbid")),
@ -386,7 +386,7 @@ class TestInteractiveRipEanFirst:
config = RipperConfig(output_dir=tmp_path)
sp = self._scanner_patches()
with (
sp[0], sp[1],
sp[0], sp[1], sp[2],
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
patch("builtins.input", side_effect=iter(inputs)),
patch("musiksammlung.ripper.lookup_by_barcode",
@ -414,7 +414,7 @@ class TestInteractiveRipEanFirst:
config = RipperConfig(output_dir=tmp_path)
sp = self._scanner_patches()
with (
sp[0], sp[1],
sp[0], sp[1], sp[2],
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
patch("builtins.input", side_effect=iter(inputs)),
patch("musiksammlung.ripper.lookup_by_barcode",
@ -440,7 +440,7 @@ class TestInteractiveRipEanFirst:
sp = self._scanner_patches()
with (
sp[0], sp[1],
sp[0], sp[1], sp[2],
patch("musiksammlung.ripper.rip_disc", return_value=(disc_dir, None, _CDDB_TRACKS)),
patch("builtins.input", side_effect=iter(inputs)),
patch("musiksammlung.ripper.lookup_by_barcode", return_value=(_MB_ALBUM, "fake-mbid")),
@ -459,7 +459,11 @@ class TestInteractiveRipEanFirst:
@staticmethod
def _scanner_patches():
"""Patches für ScannerServer und _input_or_scan (alle interactive_rip-Tests)."""
"""Patches für ScannerServer, _input_or_scan und _read_line.
_input_or_scan und _read_line leiten beide an builtins.input weiter,
das in jedem Test separat mit einer Eingabeliste gemockt wird.
"""
mock_scanner = MagicMock()
mock_scanner.url.return_value = "http://127.0.0.1:8765"
mock_scanner.get_photo.return_value = None
@ -469,6 +473,10 @@ class TestInteractiveRipEanFirst:
"musiksammlung.ripper._input_or_scan",
side_effect=lambda prompt, scanner: (input(prompt), None),
),
patch(
"musiksammlung.ripper._read_line",
side_effect=lambda prompt="": input(prompt),
),
]
@staticmethod
@ -491,7 +499,7 @@ class TestInteractiveRipEanFirst:
config = RipperConfig(output_dir=tmp_path)
patches = self._fallback_patches(inputs)
with (
patches[0], patches[1], patches[2],
patches[0], patches[1], patches[2], patches[3],
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
patch("musiksammlung.ripper.lookup_by_barcode") as mock_lookup,
):
@ -514,7 +522,7 @@ class TestInteractiveRipEanFirst:
config = RipperConfig(output_dir=tmp_path)
patches = self._fallback_patches(inputs)
with (
patches[0], patches[1], patches[2],
patches[0], patches[1], patches[2], patches[3],
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
patch(
"musiksammlung.ripper.lookup_by_barcode",
@ -539,7 +547,7 @@ class TestInteractiveRipEanFirst:
config = RipperConfig(output_dir=tmp_path)
patches = self._fallback_patches(inputs)
with (
patches[0], patches[1], patches[2],
patches[0], patches[1], patches[2], patches[3],
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
patch("musiksammlung.ripper.lookup_by_barcode"),
):
@ -565,7 +573,7 @@ class TestInteractiveRipEanFirst:
config = RipperConfig(output_dir=tmp_path)
patches = self._fallback_patches(inputs)
with (
patches[0], patches[1], patches[2],
patches[0], patches[1], patches[2], patches[3],
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
patch("musiksammlung.ripper.lookup_by_barcode"),
):