diff --git a/src/musiksammlung/ripper.py b/src/musiksammlung/ripper.py index d8e3611..4717276 100644 --- a/src/musiksammlung/ripper.py +++ b/src/musiksammlung/ripper.py @@ -5,11 +5,17 @@ from __future__ import annotations import logging import queue as _queue_module import re +import select import subprocess import sys import threading from pathlib import Path +try: + import readline # noqa: F401 — aktiviert Zeileneditor für input() +except ImportError: + pass # Fallback: input() ohne Zeileneditor + from pydantic import BaseModel from musiksammlung.cddb import get_discid, lookup_by_discid @@ -145,59 +151,40 @@ def _get_vision_result( # --------------------------------------------------------------------------- -# Shared stdin reader — ein einziger Thread liest von stdin, alle Aufrufe -# von _read_line() und _input_or_scan() nutzen dieselbe Queue. Damit gibt -# es keine verwaisten Threads, die nach einem Foto-Upload weiterhin auf -# sys.stdin blockieren und nachfolgende Eingaben "stehlen". +# Zeileneditor für Benutzereingaben +# +# _read_line(prompt) — nutzt input() auf dem Hauptthread. Durch den Import +# von readline oben stehen Pfeiltasten, Backspace, Ctrl-A/E, History +# (Pfeil hoch/runter) usw. automatisch zur Verfügung. +# +# _input_or_scan(prompt, scanner) — pollt stdin UND Scanner-Queue parallel +# via select() ohne Background-Thread. Kein readline (nur terminal- +# eigenes Editing: Backspace, Ctrl-U), dafür keine verwaisten Threads. +# Betrifft nur EAN-Prompt und Disc-Insert — dort ist Komfort-Editing +# nicht nötig (Enter / kurze Ziffern). # --------------------------------------------------------------------------- -_stdin_queue: _queue_module.Queue[str] = _queue_module.Queue() -_stdin_reader_lock = threading.Lock() -_stdin_reader_started = False - - -def _ensure_stdin_reader() -> None: - """Startet den gemeinsamen stdin-Reader-Thread (einmalig, idempotent).""" - global _stdin_reader_started - with _stdin_reader_lock: - if _stdin_reader_started: - return - _stdin_reader_started = True - - def _reader() -> None: - while True: - try: - line = sys.stdin.readline() - if not line: # EOF - _stdin_queue.put("") - break - _stdin_queue.put(line.rstrip("\n")) - except (EOFError, OSError): - _stdin_queue.put("") - break - - threading.Thread(target=_reader, daemon=True).start() - def _read_line(prompt: str = "") -> str: - """Liest eine Zeile von stdin über den Shared Reader. + """Liest eine Zeile von stdin mit readline-Unterstützung. - Ersetzt input() überall in interactive_rip, damit keine konkurrierenden - Threads auf stdin warten. + Pfeiltasten, Backspace, Ctrl-A/E, History etc. funktionieren, + weil input() den GNU-readline-Editor nutzt (sofern importiert). """ - _ensure_stdin_reader() - if prompt: - print(prompt, end="", flush=True) - return _stdin_queue.get() + try: + return input(prompt) + except EOFError: + return "" def _input_or_scan( prompt: str, scanner: ScannerServer | None, ) -> tuple[str, Path | None]: - """Kombiniertes stdin + Scanner-Queue: wartet gleichzeitig auf Tastatur und Foto. + """Wartet gleichzeitig auf Tastatureingabe und Foto-Upload. - Nutzt den Shared stdin-Reader — kein eigener Thread pro Aufruf. + Nutzt select() zum nicht-blockierenden Polling — kein Background-Thread, + keine verwaisten Threads nach Foto-Upload. Returns: (eingegebener Text, None) — wenn der User Enter drückt @@ -206,7 +193,6 @@ def _input_or_scan( if scanner is None: return _clean_input(_read_line(prompt)), None - _ensure_stdin_reader() print(prompt, end="", flush=True) while True: @@ -215,11 +201,10 @@ def _input_or_scan( print("\n [Foto empfangen — weiter automatisch]", flush=True) return "", photo - try: - val = _stdin_queue.get(timeout=0.1) - return _clean_input(val), None - except _queue_module.Empty: - continue + ready, _, _ = select.select([sys.stdin], [], [], 0.1) + if ready: + line = sys.stdin.readline().rstrip("\n") + return _clean_input(line), None