232 lines
8.3 KiB
Python
232 lines
8.3 KiB
Python
|
|
"""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<Track>"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()
|