Fix file extraction: don't use abcde move, extract from temp dir ourselves

abcde's move action + OUTPUTFORMAT config failed because shell variables
like ${TRACKNUM} are evaluated immediately when the config is sourced.
Instead: skip move, search abcde's internal temp dir (abcde.XXXX/trackNN.flac)
and move files flat into output_dir ourselves.

- Replace _get_audio_files/_write_abcde_config with _extract_tracks()
- _rename_files() now matches track01.flac pattern (abcde naming)
- Fallback rename to 01.flac etc. when no CDDB data available

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-17 20:15:50 +01:00
commit f2d3684956

View file

@ -102,51 +102,37 @@ def _parse_cddb_response(output: str) -> list[TrackInfo]:
return tracks return tracks
def _get_audio_files(output_dir: Path, audio_format: AudioFormat) -> list[Path]: def _extract_tracks(output_dir: Path, audio_format: AudioFormat) -> list[Path]:
"""Find all audio files in directory recursively (case-insensitive). """Find abcde track files recursively and move them flat into output_dir.
abcde stores encoded files inside its temp dir as:
output_dir/abcde.XXXX/track01.flac
output_dir/abcde.XXXX/track02.flac ...
This function moves them to:
output_dir/track01.flac
output_dir/track02.flac ...
Args: Args:
output_dir: Target directory output_dir: Directory to search and target for flat layout
audio_format: Audio format audio_format: Audio format
Returns: Returns:
Sorted list of found files Sorted list of moved files in output_dir
""" """
# Regex pattern for case-insensitive search
ext = audio_format.extension.lstrip(".") ext = audio_format.extension.lstrip(".")
pattern = re.compile(rf".*\.{ext}$", re.IGNORECASE) # abcde names files trackNN.ext (with -p: track01, track02, ...)
pattern = re.compile(rf"^track(\d+)\.{ext}$", re.IGNORECASE)
audio_files = [] moved = []
# rglob: search recursively so abcde subdirs are also covered for file in sorted(output_dir.rglob("*")):
for file in output_dir.rglob("*"):
if file.is_file() and pattern.match(file.name): if file.is_file() and pattern.match(file.name):
audio_files.append(file) dest = output_dir / file.name
if file != dest:
file.rename(dest)
moved.append(dest)
return sorted(audio_files) return moved
def _write_abcde_config(output_dir: Path) -> Path:
"""Write a temporary abcde config file.
Sets OUTPUTDIR to output_dir and uses a flat filename format
(track number only) so we can rename files ourselves afterward.
Args:
output_dir: Directory where encoded files should be placed
Returns:
Path to the config file
"""
config = f"""\
OUTPUTDIR="{output_dir}"
OUTPUTFORMAT="${{TRACKNUM}}"
VAOUTPUTFORMAT="${{TRACKNUM}}"
ONETRACKOUTPUTFORMAT="${{TRACKNUM}}"
PLAYLISTFORMAT=""
"""
config_path = output_dir / ".abcde.conf"
config_path.write_text(config, encoding="utf-8")
return config_path
def _rename_files( def _rename_files(
@ -154,26 +140,26 @@ def _rename_files(
tracks: list[TrackInfo], tracks: list[TrackInfo],
audio_format: AudioFormat, audio_format: AudioFormat,
) -> None: ) -> None:
"""Rename files according to naming scheme. """Rename track files according to naming scheme.
Format: <two-digit track_number>_-_title_-_artist.extension Expected input: track01.flac, track02.flac, ...
Output: 01_-_title_-_artist.flac, 02_-_title_-_artist.flac, ...
Args: Args:
output_dir: Directory with files output_dir: Directory with files
tracks: Track information tracks: Track information from CDDB
audio_format: Audio format audio_format: Audio format
""" """
audio_files = _get_audio_files(output_dir, audio_format) ext = audio_format.extension.lstrip(".")
# Matches track01.flac, track02.flac, ... (abcde naming)
abcde_pattern = re.compile(rf"^track(\d+)\.{ext}$", re.IGNORECASE)
# Pattern for abcde filenames: 01, 02, ..., 10, 11, ... audio_files = sorted(output_dir.glob(f"track*.{ext}"))
abcde_pattern = re.compile(r"^(\d+)\.")
for track in tracks: for track in tracks:
# Find matching file
for file in audio_files: for file in audio_files:
match = abcde_pattern.match(file.name) match = abcde_pattern.match(file.name)
if match and int(match.group(1)) == track.track_number: if match and int(match.group(1)) == track.track_number:
# New name: <two-digit track_number>_-_title_-_artist.extension
track_num_padded = f"{track.track_number:02d}" track_num_padded = f"{track.track_number:02d}"
artist_clean = _sanitize_name(track.artist) artist_clean = _sanitize_name(track.artist)
title_clean = _sanitize_name(track.title) title_clean = _sanitize_name(track.title)
@ -181,14 +167,22 @@ def _rename_files(
f"{track_num_padded}_-_{title_clean}_-_" f"{track_num_padded}_-_{title_clean}_-_"
f"{artist_clean}{audio_format.extension}" f"{artist_clean}{audio_format.extension}"
) )
new_path = output_dir / new_name new_path = output_dir / new_name
if file != new_path: if file != new_path:
logger.info("Renaming: %s -> %s", file.name, new_name) logger.info("Renaming: %s -> %s", file.name, new_name)
file.rename(new_path) file.rename(new_path)
break break
# Rename remaining track files without CDDB info (fallback: 01.flac, ...)
for file in sorted(output_dir.glob(f"track*.{ext}")):
match = abcde_pattern.match(file.name)
if match:
num = int(match.group(1))
new_path = output_dir / f"{num:02d}{audio_format.extension}"
if file != new_path:
logger.info("Renaming (no CDDB): %s -> %s", file.name, new_path.name)
file.rename(new_path)
def _rip_with_abcde( def _rip_with_abcde(
device: str, device: str,
@ -215,12 +209,8 @@ def _rip_with_abcde(
""" """
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
# Write abcde config: controls OUTPUTDIR and flat filename format
config_path = _write_abcde_config(output_dir)
# abcde options: # abcde options:
# -c config: use our config (OUTPUTDIR, OUTPUTFORMAT) # -a actions: cddb+read+encode+tag (no 'move' — we extract files ourselves)
# -a actions: cddb+read+encode+tag+move, or read+encode+move
# -p: pad track numbers with zeros # -p: pad track numbers with zeros
# -o format: output format # -o format: output format
# -d device: CD drive # -d device: CD drive
@ -228,7 +218,6 @@ def _rip_with_abcde(
# -N: non-interactive (no prompts, auto-select first CDDB match) # -N: non-interactive (no prompts, auto-select first CDDB match)
cmd = [ cmd = [
"abcde", "abcde",
"-c", str(config_path),
"-p", "-p",
"-o", audio_format.get_abcde_format(), "-o", audio_format.get_abcde_format(),
"-d", device, "-d", device,
@ -236,11 +225,10 @@ def _rip_with_abcde(
"-N", "-N",
] ]
# Actions — move is required so files land in OUTPUTDIR
if use_cddb: if use_cddb:
cmd.extend(["-a", "cddb,read,encode,tag,move"]) cmd.extend(["-a", "cddb,read,encode,tag"])
else: else:
cmd.extend(["-a", "read,encode,move"]) cmd.extend(["-a", "read,encode"])
# Parallel encodes # Parallel encodes
if parallel_jobs > 1: if parallel_jobs > 1:
@ -290,8 +278,8 @@ def _rip_with_abcde(
if tracks: if tracks:
logger.info("CDDB data found: %d tracks", len(tracks)) logger.info("CDDB data found: %d tracks", len(tracks))
# Find files (case-insensitive, recursive) # Extract track files from abcde's temp dir into output_dir (flat)
audio_files = _get_audio_files(output_dir, audio_format) audio_files = _extract_tracks(output_dir, audio_format)
if not audio_files: if not audio_files:
raise RuntimeError( raise RuntimeError(