Tagger- und CLI-Tests; Bugfix embed_cover für MP3 ohne ID3-Header
- tests/test_tagger.py: 20 Tests für tag_file, tag_album, _scale_cover_for_embed, embed_cover (FLAC + MP3), embed_album_cover - tests/test_cli.py: 14 Tests für apply (in-place, disc-mismatch, dry-run, playlist, multi-disc), check und scan (via Mock) - tagger.py: embed_cover für MP3 fängt ID3NoHeaderError ab und erstellt einen neuen ID3-Tag wenn keiner vorhanden ist Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
70c096cde4
commit
cfc2a2018e
3 changed files with 548 additions and 3 deletions
|
|
@ -8,7 +8,7 @@ from pathlib import Path
|
|||
|
||||
from mutagen import File as MutagenFile
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.id3 import APIC, ID3
|
||||
from mutagen.id3 import APIC, ID3, ID3NoHeaderError
|
||||
from PIL import Image
|
||||
|
||||
from musiksammlung.models import Album, Disc, Track
|
||||
|
|
@ -114,7 +114,10 @@ def embed_cover(audio_path: Path, cover_path: Path) -> None:
|
|||
audio.save()
|
||||
|
||||
elif suffix == ".mp3":
|
||||
audio = ID3(str(audio_path))
|
||||
try:
|
||||
audio = ID3(str(audio_path))
|
||||
except ID3NoHeaderError:
|
||||
audio = ID3()
|
||||
audio.add(APIC(
|
||||
encoding=3,
|
||||
mime=mime,
|
||||
|
|
@ -122,7 +125,7 @@ def embed_cover(audio_path: Path, cover_path: Path) -> None:
|
|||
desc="Front cover",
|
||||
data=cover_data,
|
||||
))
|
||||
audio.save()
|
||||
audio.save(str(audio_path))
|
||||
|
||||
else:
|
||||
logger.debug("Cover-Embedding für %s nicht unterstützt", suffix)
|
||||
|
|
|
|||
212
tests/test_cli.py
Normal file
212
tests/test_cli.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"""Tests für die CLI-Commands (cli.py) via typer.testing.CliRunner."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from musiksammlung.cli import app
|
||||
from musiksammlung.models import Album, Disc, Track
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen (identisch zu test_tagger.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_flac(path: Path) -> Path:
|
||||
data = b"fLaC"
|
||||
data += b"\x80\x00\x00\x22"
|
||||
data += struct.pack(">HH", 4096, 4096)
|
||||
data += b"\x00\x00\x00\x00\x00\x00"
|
||||
val = (44100 << 44) | (0 << 41) | (15 << 36) | 0
|
||||
data += val.to_bytes(8, "big")
|
||||
data += b"\x00" * 16
|
||||
path.write_bytes(data)
|
||||
return path
|
||||
|
||||
|
||||
def _make_album_json(path: Path, *, n_tracks: int = 2, n_discs: int = 1) -> Album:
|
||||
discs = [
|
||||
Disc(
|
||||
disc_number=d,
|
||||
tracks=[
|
||||
Track(track_number=t, title=f"Disc{d} Track{t}") for t in range(1, n_tracks + 1)
|
||||
],
|
||||
)
|
||||
for d in range(1, n_discs + 1)
|
||||
]
|
||||
album = Album(artist="TestArtist", album="TestAlbum", year=2024, discs=discs)
|
||||
path.write_text(album.model_dump_json(indent=2), encoding="utf-8")
|
||||
return album
|
||||
|
||||
|
||||
def _make_disc_files(album_dir: Path, album: Album) -> None:
|
||||
"""Erstellt FLAC-Dateien im korrekten Namensschema."""
|
||||
multi = len(album.discs) > 1
|
||||
for disc in album.discs:
|
||||
disc_dir = album_dir / f"CD{disc.disc_number}" if multi else album_dir
|
||||
disc_dir.mkdir(parents=True, exist_ok=True)
|
||||
for track in disc.tracks:
|
||||
fname = f"{track.track_number:02d}_-_{track.title.replace(' ', '_')}_-_TestArtist.flac"
|
||||
_make_flac(disc_dir / fname)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# apply
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestApplyCommand:
|
||||
def test_apply_inplace_renames_and_tags(self, tmp_path: Path) -> None:
|
||||
_make_album_json(tmp_path / "album.json")
|
||||
# Audiodateien mit track*-Namen (wie nach CDDB-Fehler)
|
||||
_make_flac(tmp_path / "track01.flac")
|
||||
_make_flac(tmp_path / "track02.flac")
|
||||
|
||||
result = runner.invoke(app, [
|
||||
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
def test_apply_fails_on_disc_mismatch(self, tmp_path: Path) -> None:
|
||||
_make_album_json(tmp_path / "album.json", n_tracks=3)
|
||||
# Nur 2 Dateien, JSON hat 3 Tracks
|
||||
_make_flac(tmp_path / "track01.flac")
|
||||
_make_flac(tmp_path / "track02.flac")
|
||||
|
||||
result = runner.invoke(app, [
|
||||
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||||
])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "Diskrepanz" in result.output or "Diskrepanz" in (result.stderr or "")
|
||||
|
||||
def test_apply_dry_run_makes_no_changes(self, tmp_path: Path) -> None:
|
||||
_make_album_json(tmp_path / "album.json")
|
||||
f1 = _make_flac(tmp_path / "track01.flac")
|
||||
f2 = _make_flac(tmp_path / "track02.flac")
|
||||
|
||||
result = runner.invoke(app, [
|
||||
"apply", str(tmp_path), str(tmp_path / "album.json"),
|
||||
"--in-place", "--dry-run",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "DRY-RUN" in result.output
|
||||
# Originaldateien noch vorhanden
|
||||
assert f1.exists()
|
||||
assert f2.exists()
|
||||
|
||||
def test_apply_missing_json_raises(self, tmp_path: Path) -> None:
|
||||
result = runner.invoke(app, [
|
||||
"apply", str(tmp_path), str(tmp_path / "nope.json"), "--in-place",
|
||||
])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_apply_creates_playlist(self, tmp_path: Path) -> None:
|
||||
album = _make_album_json(tmp_path / "album.json")
|
||||
_make_disc_files(tmp_path, album)
|
||||
|
||||
result = runner.invoke(app, [
|
||||
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
playlists = list(tmp_path.glob("*.m3u"))
|
||||
assert len(playlists) == 1
|
||||
|
||||
def test_apply_multi_disc(self, tmp_path: Path) -> None:
|
||||
album = _make_album_json(tmp_path / "album.json", n_tracks=2, n_discs=2)
|
||||
_make_disc_files(tmp_path, album)
|
||||
|
||||
result = runner.invoke(app, [
|
||||
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckCommand:
|
||||
def test_check_shows_cover_status(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "frontcover.jpg").write_bytes(b"\xff\xd8\xff\xe0") # minimal JPEG magic
|
||||
|
||||
result = runner.invoke(app, ["check", str(tmp_path)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "frontcover.jpg" in result.output
|
||||
|
||||
def test_check_shows_missing_cover(self, tmp_path: Path) -> None:
|
||||
result = runner.invoke(app, ["check", str(tmp_path)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "fehlt" in result.output
|
||||
|
||||
def test_check_lists_audio_files(self, tmp_path: Path) -> None:
|
||||
_make_flac(tmp_path / "01_-_Track_-_Artist.flac")
|
||||
|
||||
result = runner.invoke(app, ["check", str(tmp_path)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "01_-_Track_-_Artist.flac" in result.output
|
||||
|
||||
def test_check_nonexistent_directory(self, tmp_path: Path) -> None:
|
||||
result = runner.invoke(app, ["check", str(tmp_path / "nope")])
|
||||
assert result.exit_code == 1
|
||||
|
||||
def test_check_no_audio_files_message(self, tmp_path: Path) -> None:
|
||||
result = runner.invoke(app, ["check", str(tmp_path)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Keine Audiodateien" in result.output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# scan (mockt LLM-Aufruf)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestScanCommand:
|
||||
def test_scan_from_text_creates_json(self, tmp_path: Path) -> None:
|
||||
text_file = tmp_path / "tracklist.txt"
|
||||
text_file.write_text("1. Allegro\n2. Andante", encoding="utf-8")
|
||||
output = tmp_path / "album.json"
|
||||
|
||||
fake_album = Album(
|
||||
artist="Karajan",
|
||||
album="Beethoven",
|
||||
year=1963,
|
||||
discs=[Disc(disc_number=1, tracks=[Track(track_number=1, title="Allegro")])],
|
||||
)
|
||||
|
||||
with patch("musiksammlung.cli.parse_tracklist", return_value=fake_album):
|
||||
result = runner.invoke(app, [
|
||||
"scan", "--from-text", str(text_file), "--output", str(output),
|
||||
])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert output.exists()
|
||||
data = json.loads(output.read_text())
|
||||
assert data["artist"] == "Karajan"
|
||||
|
||||
def test_scan_missing_text_file(self, tmp_path: Path) -> None:
|
||||
result = runner.invoke(app, [
|
||||
"scan", "--from-text", str(tmp_path / "nope.txt"),
|
||||
])
|
||||
assert result.exit_code == 1
|
||||
|
||||
def test_scan_no_args_fails(self) -> None:
|
||||
result = runner.invoke(app, ["scan"])
|
||||
assert result.exit_code == 1
|
||||
330
tests/test_tagger.py
Normal file
330
tests/test_tagger.py
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
"""Tests für Audio-Tagging und Cover-Embedding (tagger.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
from mutagen import File as MutagenFile
|
||||
from mutagen.flac import FLAC
|
||||
from mutagen.id3 import ID3
|
||||
from PIL import Image
|
||||
|
||||
from musiksammlung.models import Album, Disc, Track
|
||||
from musiksammlung.tagger import (
|
||||
EMBED_COVER_MAX_SIZE,
|
||||
_scale_cover_for_embed,
|
||||
embed_album_cover,
|
||||
embed_cover,
|
||||
tag_album,
|
||||
tag_file,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_flac(path: Path) -> Path:
|
||||
"""Erstellt eine minimale gültige FLAC-Datei (nur STREAMINFO-Block, keine Audio-Daten)."""
|
||||
data = b"fLaC"
|
||||
# STREAMINFO: last=1 (0x80), type=0, length=34 (0x000022)
|
||||
data += b"\x80\x00\x00\x22"
|
||||
data += struct.pack(">HH", 4096, 4096) # min/max blocksize
|
||||
data += b"\x00\x00\x00\x00\x00\x00" # min/max framesize
|
||||
# 20-bit sample_rate=44100, 3-bit channels-1=0, 5-bit bps-1=15, 36-bit total_samples=0
|
||||
val = (44100 << 44) | (0 << 41) | (15 << 36) | 0
|
||||
data += val.to_bytes(8, "big")
|
||||
data += b"\x00" * 16 # MD5 (Nullen = unbekannt)
|
||||
path.write_bytes(data)
|
||||
return path
|
||||
|
||||
|
||||
def _make_mp3(path: Path) -> Path:
|
||||
"""Erstellt eine minimale gültige MP3-Datei (MPEG1 Layer3, 128kbps, 44100Hz, mono)."""
|
||||
# Frame-Header: 0xFF 0xFB 0x90 0xC4
|
||||
# sync=0xFFF, MPEG1=11, Layer3=01, no-CRC=1
|
||||
# bitrate=1001 (128kbps), samplerate=00 (44100), no padding, private=0
|
||||
# mono=11, mode_ext=00, copyright=0, original=1, emphasis=00
|
||||
header = bytes([0xFF, 0xFB, 0x90, 0xC4])
|
||||
frame_size = 417 # 144 * 128000 / 44100 = 417 Bytes
|
||||
frame = header + bytes(frame_size - 4)
|
||||
path.write_bytes(frame * 4) # 4 aufeinanderfolgende Frames → mutagen findet Sync
|
||||
return path
|
||||
|
||||
|
||||
def _make_cover(path: Path, size: tuple[int, int] = (100, 100), mode: str = "RGB") -> Path:
|
||||
"""Erstellt ein einfarbiges Test-Cover-Bild."""
|
||||
img = Image.new(mode, size, color=(200, 100, 50))
|
||||
fmt = "JPEG" if path.suffix.lower() in (".jpg", ".jpeg") else "PNG"
|
||||
if mode == "RGBA" and fmt == "JPEG":
|
||||
img = img.convert("RGB")
|
||||
img.save(str(path), fmt)
|
||||
return path
|
||||
|
||||
|
||||
def _make_album(
|
||||
artist: str = "TestArtist",
|
||||
tracks: int = 2,
|
||||
year: int | None = 2024,
|
||||
) -> Album:
|
||||
return Album(
|
||||
artist=artist,
|
||||
album="TestAlbum",
|
||||
year=year,
|
||||
discs=[
|
||||
Disc(
|
||||
disc_number=1,
|
||||
tracks=[Track(track_number=i, title=f"Track {i}") for i in range(1, tracks + 1)],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tag_file
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTagFile:
|
||||
def test_sets_basic_tags_on_flac(self, tmp_path: Path) -> None:
|
||||
path = _make_flac(tmp_path / "t.flac")
|
||||
album = _make_album()
|
||||
disc = album.discs[0]
|
||||
track = disc.tracks[0]
|
||||
|
||||
tag_file(path, album, disc, track)
|
||||
|
||||
audio = MutagenFile(str(path), easy=True)
|
||||
assert audio["title"] == ["Track 1"]
|
||||
assert audio["artist"] == ["TestArtist"]
|
||||
assert audio["albumartist"] == ["TestArtist"]
|
||||
assert audio["album"] == ["TestAlbum"]
|
||||
assert audio["tracknumber"] == ["1/2"]
|
||||
assert audio["discnumber"] == ["1"]
|
||||
assert audio["date"] == ["2024"]
|
||||
|
||||
def test_track_artist_overrides_album_artist(self, tmp_path: Path) -> None:
|
||||
path = _make_flac(tmp_path / "t.flac")
|
||||
album = _make_album("AlbumArtist")
|
||||
disc = album.discs[0]
|
||||
track = Track(track_number=1, title="Solo", artist="TrackArtist")
|
||||
|
||||
tag_file(path, album, disc, track)
|
||||
|
||||
audio = MutagenFile(str(path), easy=True)
|
||||
assert audio["artist"] == ["TrackArtist"]
|
||||
assert audio["albumartist"] == ["AlbumArtist"]
|
||||
|
||||
def test_year_omitted_when_none(self, tmp_path: Path) -> None:
|
||||
path = _make_flac(tmp_path / "t.flac")
|
||||
album = _make_album(year=None)
|
||||
disc = album.discs[0]
|
||||
track = disc.tracks[0]
|
||||
|
||||
tag_file(path, album, disc, track)
|
||||
|
||||
audio = MutagenFile(str(path), easy=True)
|
||||
assert audio.get("date") is None
|
||||
|
||||
def test_tracknumber_includes_total(self, tmp_path: Path) -> None:
|
||||
path = _make_flac(tmp_path / "t.flac")
|
||||
album = _make_album(tracks=5)
|
||||
disc = album.discs[0]
|
||||
|
||||
tag_file(path, album, disc, disc.tracks[2]) # Track 3
|
||||
|
||||
audio = MutagenFile(str(path), easy=True)
|
||||
assert audio["tracknumber"] == ["3/5"]
|
||||
|
||||
def test_sets_tags_on_mp3(self, tmp_path: Path) -> None:
|
||||
path = _make_mp3(tmp_path / "t.mp3")
|
||||
album = _make_album()
|
||||
disc = album.discs[0]
|
||||
track = disc.tracks[0]
|
||||
|
||||
tag_file(path, album, disc, track)
|
||||
|
||||
audio = MutagenFile(str(path), easy=True)
|
||||
assert audio["title"] == ["Track 1"]
|
||||
assert audio["artist"] == ["TestArtist"]
|
||||
|
||||
def test_ignores_unknown_file(self, tmp_path: Path) -> None:
|
||||
path = tmp_path / "t.xyz"
|
||||
path.write_bytes(b"not audio")
|
||||
album = _make_album()
|
||||
disc = album.discs[0]
|
||||
track = disc.tracks[0]
|
||||
# Darf nicht crashen
|
||||
tag_file(path, album, disc, track)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tag_album
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTagAlbum:
|
||||
def test_tags_all_tracks_single_disc(self, tmp_path: Path) -> None:
|
||||
_make_flac(tmp_path / "01_-_Track_1_-_TestArtist.flac")
|
||||
_make_flac(tmp_path / "02_-_Track_2_-_TestArtist.flac")
|
||||
album = _make_album(tracks=2)
|
||||
|
||||
tag_album(album, tmp_path)
|
||||
|
||||
audio = MutagenFile(str(tmp_path / "01_-_Track_1_-_TestArtist.flac"), easy=True)
|
||||
assert audio["tracknumber"] == ["1/2"]
|
||||
assert audio["title"] == ["Track 1"]
|
||||
audio2 = MutagenFile(str(tmp_path / "02_-_Track_2_-_TestArtist.flac"), easy=True)
|
||||
assert audio2["tracknumber"] == ["2/2"]
|
||||
|
||||
def test_tags_multi_disc(self, tmp_path: Path) -> None:
|
||||
album = Album(
|
||||
artist="Artist",
|
||||
album="Multi",
|
||||
year=2000,
|
||||
discs=[
|
||||
Disc(disc_number=1, tracks=[Track(track_number=1, title="A")]),
|
||||
Disc(disc_number=2, tracks=[Track(track_number=1, title="B")]),
|
||||
],
|
||||
)
|
||||
cd1 = tmp_path / "CD1"
|
||||
cd2 = tmp_path / "CD2"
|
||||
cd1.mkdir()
|
||||
cd2.mkdir()
|
||||
_make_flac(cd1 / "01_-_A_-_Artist.flac")
|
||||
_make_flac(cd2 / "01_-_B_-_Artist.flac")
|
||||
|
||||
tag_album(album, tmp_path)
|
||||
|
||||
a1 = MutagenFile(str(cd1 / "01_-_A_-_Artist.flac"), easy=True)
|
||||
assert a1["discnumber"] == ["1"]
|
||||
a2 = MutagenFile(str(cd2 / "01_-_B_-_Artist.flac"), easy=True)
|
||||
assert a2["discnumber"] == ["2"]
|
||||
|
||||
def test_missing_file_does_not_crash(self, tmp_path: Path) -> None:
|
||||
album = _make_album()
|
||||
tag_album(album, tmp_path) # Keine Dateien → kein Fehler
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _scale_cover_for_embed
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestScaleCoverForEmbed:
|
||||
def test_returns_jpeg_bytes_and_mime(self, tmp_path: Path) -> None:
|
||||
cover = _make_cover(tmp_path / "c.jpg", size=(100, 100))
|
||||
data, mime = _scale_cover_for_embed(cover, 500)
|
||||
|
||||
assert mime == "image/jpeg"
|
||||
assert len(data) > 0
|
||||
assert Image.open(io.BytesIO(data)).format == "JPEG"
|
||||
|
||||
def test_scales_down_large_image(self, tmp_path: Path) -> None:
|
||||
cover = _make_cover(tmp_path / "c.jpg", size=(1200, 1200))
|
||||
data, _ = _scale_cover_for_embed(cover, 500)
|
||||
|
||||
assert max(Image.open(io.BytesIO(data)).size) <= 500
|
||||
|
||||
def test_does_not_upscale_small_image(self, tmp_path: Path) -> None:
|
||||
cover = _make_cover(tmp_path / "c.jpg", size=(200, 200))
|
||||
data, _ = _scale_cover_for_embed(cover, 500)
|
||||
|
||||
assert max(Image.open(io.BytesIO(data)).size) <= 200
|
||||
|
||||
def test_converts_rgba_to_rgb(self, tmp_path: Path) -> None:
|
||||
cover = tmp_path / "c.png"
|
||||
Image.new("RGBA", (100, 100), (255, 0, 0, 128)).save(str(cover), "PNG")
|
||||
data, _ = _scale_cover_for_embed(cover, 500)
|
||||
|
||||
assert Image.open(io.BytesIO(data)).mode == "RGB"
|
||||
|
||||
def test_uses_embed_cover_max_size_constant(self, tmp_path: Path) -> None:
|
||||
cover = _make_cover(tmp_path / "c.jpg", size=(2000, 2000))
|
||||
data, _ = _scale_cover_for_embed(cover, EMBED_COVER_MAX_SIZE)
|
||||
|
||||
assert max(Image.open(io.BytesIO(data)).size) <= EMBED_COVER_MAX_SIZE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# embed_cover
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEmbedCover:
|
||||
def test_embeds_cover_in_flac(self, tmp_path: Path) -> None:
|
||||
path = _make_flac(tmp_path / "t.flac")
|
||||
cover = _make_cover(tmp_path / "c.jpg")
|
||||
|
||||
embed_cover(path, cover)
|
||||
|
||||
assert bool(FLAC(str(path)).pictures)
|
||||
|
||||
def test_flac_picture_type_is_front_cover(self, tmp_path: Path) -> None:
|
||||
path = _make_flac(tmp_path / "t.flac")
|
||||
cover = _make_cover(tmp_path / "c.jpg")
|
||||
|
||||
embed_cover(path, cover)
|
||||
|
||||
pic = FLAC(str(path)).pictures[0]
|
||||
assert pic.type == 3 # Front cover
|
||||
assert pic.mime == "image/jpeg"
|
||||
|
||||
def test_embeds_cover_in_mp3(self, tmp_path: Path) -> None:
|
||||
path = _make_mp3(tmp_path / "t.mp3")
|
||||
cover = _make_cover(tmp_path / "c.jpg")
|
||||
|
||||
embed_cover(path, cover)
|
||||
|
||||
tags = ID3(str(path))
|
||||
assert any(k.startswith("APIC") for k in tags.keys())
|
||||
|
||||
def test_unsupported_format_does_not_crash(self, tmp_path: Path) -> None:
|
||||
path = tmp_path / "t.ogg"
|
||||
path.write_bytes(b"\x00" * 64)
|
||||
cover = _make_cover(tmp_path / "c.jpg")
|
||||
|
||||
embed_cover(path, cover) # Nur Debug-Log, kein Fehler
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# embed_album_cover
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEmbedAlbumCover:
|
||||
def test_embeds_cover_for_all_tracks(self, tmp_path: Path) -> None:
|
||||
album = _make_album(tracks=2)
|
||||
_make_flac(tmp_path / "01_-_Track_1_-_TestArtist.flac")
|
||||
_make_flac(tmp_path / "02_-_Track_2_-_TestArtist.flac")
|
||||
cover = _make_cover(tmp_path / "frontcover.jpg")
|
||||
|
||||
embed_album_cover(album, tmp_path, cover)
|
||||
|
||||
for fname in ["01_-_Track_1_-_TestArtist.flac", "02_-_Track_2_-_TestArtist.flac"]:
|
||||
assert bool(FLAC(str(tmp_path / fname)).pictures), f"Cover fehlt in {fname}"
|
||||
|
||||
def test_embeds_cover_multi_disc(self, tmp_path: Path) -> None:
|
||||
album = Album(
|
||||
artist="A",
|
||||
album="B",
|
||||
year=2000,
|
||||
discs=[
|
||||
Disc(disc_number=1, tracks=[Track(track_number=1, title="T1")]),
|
||||
Disc(disc_number=2, tracks=[Track(track_number=1, title="T2")]),
|
||||
],
|
||||
)
|
||||
cd1 = tmp_path / "CD1"
|
||||
cd2 = tmp_path / "CD2"
|
||||
cd1.mkdir()
|
||||
cd2.mkdir()
|
||||
_make_flac(cd1 / "01_-_T1_-_A.flac")
|
||||
_make_flac(cd2 / "01_-_T2_-_A.flac")
|
||||
cover = _make_cover(tmp_path / "frontcover.jpg")
|
||||
|
||||
embed_album_cover(album, tmp_path, cover)
|
||||
|
||||
assert bool(FLAC(str(cd1 / "01_-_T1_-_A.flac")).pictures)
|
||||
assert bool(FLAC(str(cd2 / "01_-_T2_-_A.flac")).pictures)
|
||||
Loading…
Add table
Add a link
Reference in a new issue