diff --git a/src/musiksammlung/ripper.py b/src/musiksammlung/ripper.py index 0f59f62..adc9d01 100644 --- a/src/musiksammlung/ripper.py +++ b/src/musiksammlung/ripper.py @@ -102,51 +102,37 @@ def _parse_cddb_response(output: str) -> list[TrackInfo]: return tracks -def _get_audio_files(output_dir: Path, audio_format: AudioFormat) -> list[Path]: - """Find all audio files in directory recursively (case-insensitive). +def _extract_tracks(output_dir: Path, audio_format: AudioFormat) -> list[Path]: + """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: - output_dir: Target directory + output_dir: Directory to search and target for flat layout audio_format: Audio format 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(".") - 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 = [] - # rglob: search recursively so abcde subdirs are also covered - for file in output_dir.rglob("*"): + moved = [] + for file in sorted(output_dir.rglob("*")): 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) - - -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 + return moved def _rename_files( @@ -154,26 +140,26 @@ def _rename_files( tracks: list[TrackInfo], audio_format: AudioFormat, ) -> None: - """Rename files according to naming scheme. + """Rename track files according to naming scheme. - Format: _-_title_-_artist.extension + Expected input: track01.flac, track02.flac, ... + Output: 01_-_title_-_artist.flac, 02_-_title_-_artist.flac, ... Args: output_dir: Directory with files - tracks: Track information + tracks: Track information from CDDB 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, ... - abcde_pattern = re.compile(r"^(\d+)\.") + audio_files = sorted(output_dir.glob(f"track*.{ext}")) for track in tracks: - # Find matching file for file in audio_files: match = abcde_pattern.match(file.name) if match and int(match.group(1)) == track.track_number: - # New name: _-_title_-_artist.extension track_num_padded = f"{track.track_number:02d}" artist_clean = _sanitize_name(track.artist) title_clean = _sanitize_name(track.title) @@ -181,14 +167,22 @@ def _rename_files( 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}")): + 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( device: str, @@ -215,12 +209,8 @@ def _rip_with_abcde( """ 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: - # -c config: use our config (OUTPUTDIR, OUTPUTFORMAT) - # -a actions: cddb+read+encode+tag+move, or read+encode+move + # -a actions: cddb+read+encode+tag (no 'move' — we extract files ourselves) # -p: pad track numbers with zeros # -o format: output format # -d device: CD drive @@ -228,7 +218,6 @@ def _rip_with_abcde( # -N: non-interactive (no prompts, auto-select first CDDB match) cmd = [ "abcde", - "-c", str(config_path), "-p", "-o", audio_format.get_abcde_format(), "-d", device, @@ -236,11 +225,10 @@ def _rip_with_abcde( "-N", ] - # Actions — move is required so files land in OUTPUTDIR if use_cddb: - cmd.extend(["-a", "cddb,read,encode,tag,move"]) + cmd.extend(["-a", "cddb,read,encode,tag"]) else: - cmd.extend(["-a", "read,encode,move"]) + cmd.extend(["-a", "read,encode"]) # Parallel encodes if parallel_jobs > 1: @@ -290,8 +278,8 @@ def _rip_with_abcde( if tracks: logger.info("CDDB data found: %d tracks", len(tracks)) - # Find files (case-insensitive, recursive) - audio_files = _get_audio_files(output_dir, audio_format) + # Extract track files from abcde's temp dir into output_dir (flat) + audio_files = _extract_tracks(output_dir, audio_format) if not audio_files: raise RuntimeError(