Add live progress, progress bar and CDDB logging to ripper
- Replace capture_output=True with Popen+live streaming (_stream_abcde) - Show track counter: "Track 3/14 Title..." - Show cdparanoia progress bar: [████████░░░░░░░░░░░░░░░░░░░░░░] 45.2% 12.3 MB - Show CDDB album header and track list as they appear - Show tagging progress: "Tagging 14/14" - Print abcde command for full transparency - Collect CDDB track lines while streaming for later parsing - Log warnings when CDDB returns no data - Print full renamed file list after ripping Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d0d64da12e
commit
430775adf8
1 changed files with 171 additions and 98 deletions
|
|
@ -50,11 +50,8 @@ def _clean_input(raw: str) -> str:
|
||||||
Returns:
|
Returns:
|
||||||
Cleaned string
|
Cleaned string
|
||||||
"""
|
"""
|
||||||
# Remove ANSI escape sequences (\x1b[... and ^[[...)
|
|
||||||
cleaned = _ANSI_ESC.sub("", raw)
|
cleaned = _ANSI_ESC.sub("", raw)
|
||||||
# Remove remaining control characters (backspace \x08, etc.)
|
|
||||||
cleaned = re.sub(r"[\x00-\x1f\x7f]", "", cleaned)
|
cleaned = re.sub(r"[\x00-\x1f\x7f]", "", cleaned)
|
||||||
# Strip surrounding whitespace and quotes
|
|
||||||
cleaned = cleaned.strip().strip('"\'')
|
cleaned = cleaned.strip().strip('"\'')
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
@ -68,50 +65,147 @@ def _sanitize_name(name: str) -> str:
|
||||||
Returns:
|
Returns:
|
||||||
Cleaned name (spaces -> underscores)
|
Cleaned name (spaces -> underscores)
|
||||||
"""
|
"""
|
||||||
# Replace spaces with underscores
|
|
||||||
name = name.replace(" ", "_")
|
name = name.replace(" ", "_")
|
||||||
# Keep umlauts and special characters
|
|
||||||
# Only remove problematic filename characters
|
|
||||||
name = re.sub(r'[<>:"/\\|?*]', "", name)
|
name = re.sub(r'[<>:"/\\|?*]', "", name)
|
||||||
# Remove leading/trailing underscores
|
|
||||||
name = name.strip("_")
|
name = name.strip("_")
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
def _parse_cddb_response(output: str) -> list[TrackInfo]:
|
def _parse_cddb_lines(lines: list[str]) -> list[TrackInfo]:
|
||||||
"""Parse CDDB data from abcde output.
|
"""Parse CDDB track list from abcde output lines.
|
||||||
|
|
||||||
|
Matches lines like: "1: Wolfgang Anheisser - Wer recht in Freuden wandern will"
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
output: abcde stdout/stderr output
|
lines: Lines collected from abcde stdout+stderr
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of track information
|
List of TrackInfo (may be empty if CDDB lookup failed)
|
||||||
"""
|
"""
|
||||||
tracks = []
|
tracks = []
|
||||||
# Pattern: "N: Artist - Title"
|
pattern = re.compile(r"^\s*(\d+):\s*(.+?)\s+-\s+(.+)$")
|
||||||
pattern = re.compile(r"^\s*(\d+):\s*(.+?)\s*-\s*(.+)$")
|
for line in lines:
|
||||||
|
m = pattern.match(line)
|
||||||
for line in output.split("\n"):
|
if m:
|
||||||
match = pattern.match(line)
|
tracks.append(TrackInfo(
|
||||||
if match:
|
track_number=int(m.group(1)),
|
||||||
track_num = int(match.group(1))
|
artist=m.group(2).strip(),
|
||||||
artist = match.group(2).strip()
|
title=m.group(3).strip(),
|
||||||
title = match.group(3).strip()
|
))
|
||||||
tracks.append(TrackInfo(track_num, artist, title))
|
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_abcde(
|
||||||
|
process: subprocess.Popen,
|
||||||
|
use_cddb: bool,
|
||||||
|
) -> tuple[list[TrackInfo] | None, int]:
|
||||||
|
"""Stream abcde output live, show meaningful progress, collect CDDB data.
|
||||||
|
|
||||||
|
Filters abcde/cdparanoia output into three layers:
|
||||||
|
- Track progress: 'Grabbing track N: Title'
|
||||||
|
- Sector progress bar from cdparanoia
|
||||||
|
- CDDB/MusicBrainz info lines
|
||||||
|
|
||||||
|
Args:
|
||||||
|
process: Running abcde subprocess
|
||||||
|
use_cddb: Whether to expect and parse CDDB output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (list of TrackInfo or None, total track count)
|
||||||
|
"""
|
||||||
|
grab_re = re.compile(r"Grabbing.*track\s+(\d+)(?:\s+of\s+(\d+))?[:\s]*(.*)", re.I)
|
||||||
|
tag_re = re.compile(r"Tagging track\s+(\d+)\s+of\s+(\d+)", re.I)
|
||||||
|
sector_re = re.compile(r"\(== PROGRESS ==.*\|\s*(\d+)\s+(\d+)\s*\]")
|
||||||
|
cddb_re = re.compile(r"^\s*(\d+):\s*(.+?)\s+-\s+(.+)$")
|
||||||
|
header_re = re.compile(r"-{2,}.+-{2,}") # ---- Artist / Album ----
|
||||||
|
total_re = re.compile(r"tracks?:\s+([\d\s]+)", re.I)
|
||||||
|
|
||||||
|
all_lines: list[str] = []
|
||||||
|
cddb_lines: list[str] = []
|
||||||
|
total_tracks = 0
|
||||||
|
current_track = 0
|
||||||
|
track_end_sector = 0
|
||||||
|
|
||||||
|
for raw in process.stdout:
|
||||||
|
line = raw.rstrip("\n\r")
|
||||||
|
all_lines.append(line)
|
||||||
|
|
||||||
|
# ── Track count from "Grabbing entire CD - tracks: 01 02 03 ..."
|
||||||
|
m = total_re.search(line)
|
||||||
|
if m and total_tracks == 0:
|
||||||
|
nums = m.group(1).split()
|
||||||
|
if nums:
|
||||||
|
total_tracks = len(nums)
|
||||||
|
|
||||||
|
# ── Grab / encode progress
|
||||||
|
m = grab_re.search(line)
|
||||||
|
if m:
|
||||||
|
current_track = int(m.group(1))
|
||||||
|
if m.group(2):
|
||||||
|
total_tracks = int(m.group(2))
|
||||||
|
title = m.group(3).strip().rstrip(".")
|
||||||
|
counter = f"{current_track}/{total_tracks}" if total_tracks else str(current_track)
|
||||||
|
print(f"\n Track {counter} {title}", flush=True)
|
||||||
|
track_end_sector = 0 # reset sector bar for new track
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── Tagging progress
|
||||||
|
m = tag_re.search(line)
|
||||||
|
if m:
|
||||||
|
print(f"\r Tagging {m.group(1)}/{m.group(2)} ", flush=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── cdparanoia sector progress bar
|
||||||
|
m = sector_re.search(line)
|
||||||
|
if m:
|
||||||
|
cur = int(m.group(1))
|
||||||
|
end = int(m.group(2)) if int(m.group(2)) > 0 else cur
|
||||||
|
if track_end_sector == 0:
|
||||||
|
track_end_sector = end
|
||||||
|
pct = min(cur / track_end_sector, 1.0) if track_end_sector > 0 else 0
|
||||||
|
bar_w = 30
|
||||||
|
filled = int(pct * bar_w)
|
||||||
|
bar = "█" * filled + "░" * (bar_w - filled)
|
||||||
|
mb = cur * 2352 / 1_048_576 # rough size in MB
|
||||||
|
print(f"\r [{bar}] {pct:5.1%} {mb:5.1f} MB", end="", flush=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── CDDB / MusicBrainz album header
|
||||||
|
if header_re.search(line):
|
||||||
|
print(f"\n {line.strip()}", flush=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── CDDB track lines "1: Artist - Title"
|
||||||
|
m = cddb_re.match(line)
|
||||||
|
if m:
|
||||||
|
cddb_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── Other important info (errors, status)
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped and any(kw in line for kw in (
|
||||||
|
"Retrieved", "Selected", "Finished", "MusicBrainz",
|
||||||
|
"Error", "ERROR", "Cannot", "failed", "No tracks",
|
||||||
|
)):
|
||||||
|
print(f"\n {stripped}", flush=True)
|
||||||
|
|
||||||
|
returncode = process.wait()
|
||||||
|
|
||||||
|
# Newline after last progress bar
|
||||||
|
print(flush=True)
|
||||||
|
|
||||||
|
tracks = _parse_cddb_lines(cddb_lines) if use_cddb else None
|
||||||
|
return tracks, returncode
|
||||||
|
|
||||||
|
|
||||||
def _extract_tracks(output_dir: Path, audio_format: AudioFormat) -> list[Path]:
|
def _extract_tracks(output_dir: Path, audio_format: AudioFormat) -> list[Path]:
|
||||||
"""Find abcde track files recursively and move them flat into output_dir.
|
"""Find abcde track files recursively and move them flat into output_dir.
|
||||||
|
|
||||||
abcde stores encoded files inside its temp dir as:
|
abcde stores encoded files inside its temp dir as:
|
||||||
output_dir/abcde.XXXX/track01.flac
|
output_dir/abcde.XXXX/track01.flac
|
||||||
output_dir/abcde.XXXX/track02.flac ...
|
|
||||||
|
|
||||||
This function moves them to:
|
Moves them to:
|
||||||
output_dir/track01.flac
|
output_dir/track01.flac
|
||||||
output_dir/track02.flac ...
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
output_dir: Directory to search and target for flat layout
|
output_dir: Directory to search and target for flat layout
|
||||||
|
|
@ -121,7 +215,6 @@ def _extract_tracks(output_dir: Path, audio_format: AudioFormat) -> list[Path]:
|
||||||
Sorted list of moved files in output_dir
|
Sorted list of moved files in output_dir
|
||||||
"""
|
"""
|
||||||
ext = audio_format.extension.lstrip(".")
|
ext = audio_format.extension.lstrip(".")
|
||||||
# abcde names files trackNN.ext (with -p: track01, track02, ...)
|
|
||||||
pattern = re.compile(rf"^track(\d+)\.{ext}$", re.IGNORECASE)
|
pattern = re.compile(rf"^track(\d+)\.{ext}$", re.IGNORECASE)
|
||||||
|
|
||||||
moved = []
|
moved = []
|
||||||
|
|
@ -129,6 +222,7 @@ def _extract_tracks(output_dir: Path, audio_format: AudioFormat) -> list[Path]:
|
||||||
if file.is_file() and pattern.match(file.name):
|
if file.is_file() and pattern.match(file.name):
|
||||||
dest = output_dir / file.name
|
dest = output_dir / file.name
|
||||||
if file != dest:
|
if file != dest:
|
||||||
|
logger.info("Extracting: %s", file.name)
|
||||||
file.rename(dest)
|
file.rename(dest)
|
||||||
moved.append(dest)
|
moved.append(dest)
|
||||||
|
|
||||||
|
|
@ -142,8 +236,10 @@ def _rename_files(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Rename track files according to naming scheme.
|
"""Rename track files according to naming scheme.
|
||||||
|
|
||||||
Expected input: track01.flac, track02.flac, ...
|
Input: track01.flac, track02.flac, ...
|
||||||
Output: 01_-_title_-_artist.flac, 02_-_title_-_artist.flac, ...
|
Output: 01_-_title_-_artist.flac, ...
|
||||||
|
|
||||||
|
Falls back to plain 01.flac etc. for tracks without CDDB info.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
output_dir: Directory with files
|
output_dir: Directory with files
|
||||||
|
|
@ -151,44 +247,35 @@ def _rename_files(
|
||||||
audio_format: Audio format
|
audio_format: Audio format
|
||||||
"""
|
"""
|
||||||
ext = audio_format.extension.lstrip(".")
|
ext = audio_format.extension.lstrip(".")
|
||||||
# Matches track01.flac, track02.flac, ... (abcde naming)
|
|
||||||
abcde_pattern = re.compile(rf"^track(\d+)\.{ext}$", re.IGNORECASE)
|
abcde_pattern = re.compile(rf"^track(\d+)\.{ext}$", re.IGNORECASE)
|
||||||
|
by_num = {t.track_number: t for t in tracks}
|
||||||
|
|
||||||
audio_files = sorted(output_dir.glob(f"track*.{ext}"))
|
|
||||||
|
|
||||||
for track in tracks:
|
|
||||||
for file in audio_files:
|
|
||||||
match = abcde_pattern.match(file.name)
|
|
||||||
if match and int(match.group(1)) == track.track_number:
|
|
||||||
track_num_padded = f"{track.track_number:02d}"
|
|
||||||
artist_clean = _sanitize_name(track.artist)
|
|
||||||
title_clean = _sanitize_name(track.title)
|
|
||||||
new_name = (
|
|
||||||
f"{track_num_padded}_-_{title_clean}_-_"
|
|
||||||
f"{artist_clean}{audio_format.extension}"
|
|
||||||
)
|
|
||||||
new_path = output_dir / new_name
|
|
||||||
if file != new_path:
|
|
||||||
logger.info("Renaming: %s -> %s", file.name, new_name)
|
|
||||||
file.rename(new_path)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Rename remaining track files without CDDB info (fallback: 01.flac, ...)
|
|
||||||
for file in sorted(output_dir.glob(f"track*.{ext}")):
|
for file in sorted(output_dir.glob(f"track*.{ext}")):
|
||||||
match = abcde_pattern.match(file.name)
|
m = abcde_pattern.match(file.name)
|
||||||
if match:
|
if not m:
|
||||||
num = int(match.group(1))
|
continue
|
||||||
new_path = output_dir / f"{num:02d}{audio_format.extension}"
|
num = int(m.group(1))
|
||||||
if file != new_path:
|
track = by_num.get(num)
|
||||||
logger.info("Renaming (no CDDB): %s -> %s", file.name, new_path.name)
|
if track:
|
||||||
file.rename(new_path)
|
new_name = (
|
||||||
|
f"{num:02d}_-_{_sanitize_name(track.title)}_-_"
|
||||||
|
f"{_sanitize_name(track.artist)}{audio_format.extension}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_name = f"{num:02d}{audio_format.extension}"
|
||||||
|
|
||||||
|
new_path = output_dir / new_name
|
||||||
|
if file != new_path:
|
||||||
|
logger.info("Renaming: %s → %s", file.name, new_name)
|
||||||
|
print(f" {file.name} → {new_name}", flush=True)
|
||||||
|
file.rename(new_path)
|
||||||
|
|
||||||
|
|
||||||
def _rip_with_abcde(
|
def _rip_with_abcde(
|
||||||
device: str,
|
device: str,
|
||||||
output_dir: Path,
|
output_dir: Path,
|
||||||
audio_format: AudioFormat,
|
audio_format: AudioFormat,
|
||||||
quality: str = "medium",
|
quality: str = "high",
|
||||||
parallel_jobs: int = 1,
|
parallel_jobs: int = 1,
|
||||||
use_pipes: bool = False,
|
use_pipes: bool = False,
|
||||||
use_cddb: bool = True,
|
use_cddb: bool = True,
|
||||||
|
|
@ -215,7 +302,7 @@ def _rip_with_abcde(
|
||||||
# -o format: output format
|
# -o format: output format
|
||||||
# -d device: CD drive
|
# -d device: CD drive
|
||||||
# -x: eject CD after ripping
|
# -x: eject CD after ripping
|
||||||
# -N: non-interactive (no prompts, auto-select first CDDB match)
|
# -N: non-interactive (auto-select first CDDB match, no prompts)
|
||||||
cmd = [
|
cmd = [
|
||||||
"abcde",
|
"abcde",
|
||||||
"-p",
|
"-p",
|
||||||
|
|
@ -230,61 +317,48 @@ def _rip_with_abcde(
|
||||||
else:
|
else:
|
||||||
cmd.extend(["-a", "read,encode"])
|
cmd.extend(["-a", "read,encode"])
|
||||||
|
|
||||||
# Parallel encodes
|
|
||||||
if parallel_jobs > 1:
|
if parallel_jobs > 1:
|
||||||
cmd.extend(["-j", str(parallel_jobs)])
|
cmd.extend(["-j", str(parallel_jobs)])
|
||||||
|
|
||||||
# Use pipes
|
|
||||||
if use_pipes:
|
if use_pipes:
|
||||||
cmd.append("-P")
|
cmd.append("-P")
|
||||||
|
|
||||||
# Encoder options for quality
|
|
||||||
encoder_opts = audio_format.get_encoder_options(quality)
|
encoder_opts = audio_format.get_encoder_options(quality)
|
||||||
if encoder_opts:
|
if encoder_opts:
|
||||||
# abcde accepts encoder options with colon: -o format:options
|
|
||||||
cmd[-2] = f"{audio_format.get_abcde_format()}:{encoder_opts}"
|
cmd[-2] = f"{audio_format.get_abcde_format()}:{encoder_opts}"
|
||||||
|
|
||||||
logger.info(
|
print(f"\n Command: {' '.join(cmd)}", flush=True)
|
||||||
"Starting abcde in %s (Format: %s, Quality: %s, CDDB: %s)",
|
logger.info("Starting abcde: %s", " ".join(cmd))
|
||||||
output_dir, audio_format.value, quality, use_cddb,
|
|
||||||
)
|
|
||||||
logger.debug("Command: %s", " ".join(cmd))
|
|
||||||
|
|
||||||
# Run abcde non-interactively, capture output for CDDB parsing
|
process = subprocess.Popen(
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
cmd,
|
||||||
cwd=str(output_dir),
|
cwd=str(output_dir),
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT, # merge stderr into stdout
|
||||||
text=True,
|
text=True,
|
||||||
|
bufsize=1, # line-buffered
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log output for debugging
|
tracks, returncode = _stream_abcde(process, use_cddb)
|
||||||
if result.stdout:
|
|
||||||
logger.debug("abcde stdout:\n%s", result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
logger.debug("abcde stderr:\n%s", result.stderr)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
if returncode != 0:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"abcde failed (exit {returncode}).")
|
||||||
f"abcde failed (exit {result.returncode}).\n"
|
|
||||||
f"{result.stderr or result.stdout}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse track info from CDDB output
|
|
||||||
tracks = None
|
|
||||||
if use_cddb:
|
if use_cddb:
|
||||||
combined = result.stdout + result.stderr
|
|
||||||
tracks = _parse_cddb_response(combined)
|
|
||||||
if tracks:
|
if tracks:
|
||||||
logger.info("CDDB data found: %d tracks", len(tracks))
|
print(f"\n CDDB: {len(tracks)} tracks found", flush=True)
|
||||||
|
logger.info("CDDB data: %d tracks", len(tracks))
|
||||||
|
else:
|
||||||
|
print("\n CDDB: no track data found", flush=True)
|
||||||
|
logger.warning("CDDB lookup returned no track data")
|
||||||
|
|
||||||
# Extract track files from abcde's temp dir into output_dir (flat)
|
# Extract track files from abcde's temp dir into output_dir (flat)
|
||||||
audio_files = _extract_tracks(output_dir, audio_format)
|
audio_files = _extract_tracks(output_dir, audio_format)
|
||||||
|
|
||||||
if not audio_files:
|
if not audio_files:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"No audio files found after ripping.\n"
|
"No audio files found after ripping. "
|
||||||
"abcde output:\n" + (result.stderr or result.stdout)
|
"Check that a CD is in the drive."
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Ripping completed: %d tracks in %s", len(audio_files), output_dir)
|
logger.info("Ripping completed: %d tracks in %s", len(audio_files), output_dir)
|
||||||
|
|
@ -295,7 +369,7 @@ def rip_disc(
|
||||||
device: str,
|
device: str,
|
||||||
output_dir: Path,
|
output_dir: Path,
|
||||||
audio_format: AudioFormat = AudioFormat.FLAC,
|
audio_format: AudioFormat = AudioFormat.FLAC,
|
||||||
quality: str = "medium",
|
quality: str = "high",
|
||||||
parallel_jobs: int = 1,
|
parallel_jobs: int = 1,
|
||||||
use_pipes: bool = False,
|
use_pipes: bool = False,
|
||||||
use_cddb: bool = True,
|
use_cddb: bool = True,
|
||||||
|
|
@ -321,6 +395,7 @@ def rip_disc(
|
||||||
album_name = None
|
album_name = None
|
||||||
if tracks:
|
if tracks:
|
||||||
album_name = tracks[0].artist
|
album_name = tracks[0].artist
|
||||||
|
print("\n Renaming files ...", flush=True)
|
||||||
_rename_files(output_dir, tracks, audio_format)
|
_rename_files(output_dir, tracks, audio_format)
|
||||||
|
|
||||||
return output_dir, album_name, tracks
|
return output_dir, album_name, tracks
|
||||||
|
|
@ -372,8 +447,8 @@ def interactive_rip(config: RipperConfig) -> None:
|
||||||
/ f"CD{disc_num}"
|
/ f"CD{disc_num}"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f" Ripping to: {disc_dir.relative_to(config.output_dir)}")
|
print(f"\n Ripping to: {disc_dir}")
|
||||||
print(" (Ripping in progress, please wait...)")
|
print(" " + "-" * 50)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, detected_album, tracks = rip_disc(
|
_, detected_album, tracks = rip_disc(
|
||||||
|
|
@ -386,25 +461,23 @@ def interactive_rip(config: RipperConfig) -> None:
|
||||||
use_cddb=config.use_cddb,
|
use_cddb=config.use_cddb,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("\n " + "-" * 50)
|
||||||
if tracks:
|
if tracks:
|
||||||
print(f" ✓ CD {disc_num} ripped successfully — {len(tracks)} tracks")
|
print(f" ✓ Done — {len(tracks)} tracks")
|
||||||
first = tracks[0]
|
for t in tracks:
|
||||||
last = tracks[-1]
|
print(f" {t.track_number:2d}. {t.title} [{t.artist}]")
|
||||||
print(f" {first.track_number:2d}. {first.title} — {first.artist}")
|
|
||||||
if last != first:
|
|
||||||
print(f" {last.track_number:2d}. {last.title} — {last.artist}")
|
|
||||||
else:
|
else:
|
||||||
print(f" ✓ CD {disc_num} ripped successfully")
|
print(" ✓ Done (no CDDB data)")
|
||||||
|
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
print(f" ✗ Ripping error: {e}")
|
print(f"\n ✗ Error: {e}")
|
||||||
raw_retry = input(" Try again? (y/n): ")
|
raw_retry = input(" Try again? (y/n): ")
|
||||||
if _clean_input(raw_retry).lower() != "y":
|
if _clean_input(raw_retry).lower() != "y":
|
||||||
print(" Aborting disc.")
|
print(" Aborting disc.")
|
||||||
break
|
break
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raw_next = input(" Next CD for this album? (y/n): ")
|
raw_next = input("\n Next CD for this album? (y/n): ")
|
||||||
if _clean_input(raw_next).lower() != "y":
|
if _clean_input(raw_next).lower() != "y":
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue