"""Tests für den CD-Ripper.""" from pathlib import Path from musiksammlung.config import AudioFormat from musiksammlung.ripper import ( RipperConfig, TrackInfo, _clean_input, _extract_tracks, _parse_cddb_lines, _rename_files, _sanitize_name, ) class TestSanitizeName: """Tests für _sanitize_name.""" def test_replace_spaces(self) -> None: assert _sanitize_name("Hello World") == "Hello_World" assert _sanitize_name("Test Track Title") == "Test_Track_Title" def test_remove_invalid_chars(self) -> None: assert _sanitize_name("Test/Track:Name") == "TestTrackName" assert _sanitize_name('Test"Name') == "TestTrackName" assert _sanitize_name("Test|Track?Name*") == "TestTrackName" def test_keep_umlauts(self) -> None: assert _sanitize_name("Grüße aus Österreich") == "Grüße_aus_Österreich" assert _sanitize_name("Schöne Sänger") == "Schöne_Sänger" def test_strip_underscores(self) -> None: assert _sanitize_name("_Test_") == "Test" assert _sanitize_name(" _Test_ ") == "Test" class TestCleanInput: """Tests für _clean_input.""" def test_strips_whitespace(self) -> None: assert _clean_input(" hello ") == "hello" def test_strips_quotes(self) -> None: assert _clean_input('"Album Name"') == "Album Name" assert _clean_input("'Album Name'") == "Album Name" def test_removes_ansi_escape(self) -> None: # ESC-based sequences (e.g. \x1b[A from arrow keys) are removed assert _clean_input("\x1b[A word") == "word" assert _clean_input("\x1b[1;5D text") == "text" def test_empty_string(self) -> None: assert _clean_input("") == "" def test_plain_input_unchanged(self) -> None: assert _clean_input("Beethoven Sinfonien") == "Beethoven Sinfonien" class TestParseCddbLines: """Tests für _parse_cddb_lines.""" def test_parse_single_track(self) -> None: lines = ["1: Artist - Title"] tracks = _parse_cddb_lines(lines) assert len(tracks) == 1 assert tracks[0].track_number == 1 assert tracks[0].artist == "Artist" assert tracks[0].title == "Title" def test_parse_multiple_tracks(self) -> None: lines = [ "1: Artist One - Title One", "2: Artist Two - Title Two", "3: Artist Three - Title Three", ] tracks = _parse_cddb_lines(lines) assert len(tracks) == 3 assert tracks[2].track_number == 3 assert tracks[2].artist == "Artist Three" assert tracks[2].title == "Title Three" def test_parse_with_spaces_in_title(self) -> None: lines = ["1: Wolfgang Anheisser - Wer recht in Freuden wandern will"] tracks = _parse_cddb_lines(lines) assert tracks[0].artist == "Wolfgang Anheisser" assert tracks[0].title == "Wer recht in Freuden wandern will" def test_ignores_non_matching_lines(self) -> None: lines = [ "Retrieving CDDB info ...", "1: Artist - Title", "Tagging track 1 of 1", ] tracks = _parse_cddb_lines(lines) assert len(tracks) == 1 def test_empty_input(self) -> None: assert _parse_cddb_lines([]) == [] class TestRipperConfig: """Tests für RipperConfig.""" def test_default_config(self) -> None: config = RipperConfig() assert config.device == "/dev/cdrom" assert config.audio_format == AudioFormat.FLAC assert config.quality == "high" assert config.parallel_jobs == 1 assert config.use_pipes is False assert config.use_cddb is True def test_custom_config(self) -> None: config = RipperConfig( device="/dev/sr0", audio_format=AudioFormat.MP3, quality="low", parallel_jobs=4, use_pipes=True, use_cddb=False, ) assert config.device == "/dev/sr0" assert config.audio_format == AudioFormat.MP3 assert config.quality == "low" assert config.parallel_jobs == 4 assert config.use_pipes is True assert config.use_cddb is False class TestAudioFormat: """Tests für AudioFormat-Methoden.""" def test_abcde_format(self) -> None: assert AudioFormat.FLAC.get_abcde_format() == "flac" assert AudioFormat.MP3.get_abcde_format() == "mp3" assert AudioFormat.OPUS.get_abcde_format() == "opus" assert AudioFormat.AAC.get_abcde_format() == "m4a" assert AudioFormat.WAV.get_abcde_format() == "wav" def test_extension(self) -> None: assert AudioFormat.FLAC.extension == ".flac" assert AudioFormat.MP3.extension == ".mp3" assert AudioFormat.OPUS.extension == ".opus" assert AudioFormat.AAC.extension == ".aac" assert AudioFormat.WAV.extension == ".wav" def test_encoder_options_flac(self) -> None: assert AudioFormat.FLAC.get_encoder_options("low") == "" assert AudioFormat.FLAC.get_encoder_options("medium") == "" assert AudioFormat.FLAC.get_encoder_options("high") == "-8" def test_encoder_options_mp3(self) -> None: assert AudioFormat.MP3.get_encoder_options("low") == "-V 7" assert AudioFormat.MP3.get_encoder_options("medium") == "-V 5" assert AudioFormat.MP3.get_encoder_options("high") == "-V 0" def test_encoder_options_opus(self) -> None: assert AudioFormat.OPUS.get_encoder_options("low") == "-b 96" assert AudioFormat.OPUS.get_encoder_options("medium") == "-b 128" assert AudioFormat.OPUS.get_encoder_options("high") == "-b 192" def test_encoder_options_aac(self) -> None: assert AudioFormat.AAC.get_encoder_options("low") == "-q:a 2" assert AudioFormat.AAC.get_encoder_options("medium") == "-q:a 3" assert AudioFormat.AAC.get_encoder_options("high") == "-q:a 4" def test_encoder_options_wav(self) -> None: assert AudioFormat.WAV.get_encoder_options("low") == "" assert AudioFormat.WAV.get_encoder_options("high") == "" class TestExtractTracks: """Tests für _extract_tracks.""" def test_moves_files_from_subdir(self, tmp_path: Path) -> None: subdir = tmp_path / "abcde.XXXX" subdir.mkdir() (subdir / "track01.flac").touch() (subdir / "track02.flac").touch() result = _extract_tracks(tmp_path, AudioFormat.FLAC) assert len(result) == 2 assert (tmp_path / "track01.flac").exists() assert (tmp_path / "track02.flac").exists() # Files no longer in subdir assert not (subdir / "track01.flac").exists() def test_leaves_already_flat_files(self, tmp_path: Path) -> None: (tmp_path / "track01.flac").touch() result = _extract_tracks(tmp_path, AudioFormat.FLAC) assert len(result) == 1 def test_ignores_wrong_format(self, tmp_path: Path) -> None: (tmp_path / "track01.mp3").touch() result = _extract_tracks(tmp_path, AudioFormat.FLAC) assert result == [] def test_empty_directory(self, tmp_path: Path) -> None: result = _extract_tracks(tmp_path, AudioFormat.FLAC) assert result == [] class TestRenameFiles: """Tests für _rename_files.""" def test_renames_with_cddb_data(self, tmp_path: Path) -> None: (tmp_path / "track01.flac").touch() (tmp_path / "track02.flac").touch() tracks = [ TrackInfo(1, "Karajan", "Allegro con brio"), TrackInfo(2, "Karajan", "Andante con moto"), ] _rename_files(tmp_path, tracks, AudioFormat.FLAC) assert (tmp_path / "01_-_Allegro_con_brio_-_Karajan.flac").exists() assert (tmp_path / "02_-_Andante_con_moto_-_Karajan.flac").exists() def test_fallback_without_cddb(self, tmp_path: Path) -> None: (tmp_path / "track01.flac").touch() _rename_files(tmp_path, [], AudioFormat.FLAC) # No CDDB data → fallback to "01.flac" assert (tmp_path / "01.flac").exists() def test_sanitizes_title_in_filename(self, tmp_path: Path) -> None: (tmp_path / "track01.flac").touch() # _sanitize_name: spaces → _, then chars like :/\ are removed (not replaced) tracks = [TrackInfo(1, "Art ist", "My Title")] _rename_files(tmp_path, tracks, AudioFormat.FLAC) assert (tmp_path / "01_-_My_Title_-_Art_ist.flac").exists()