Web frontend

This commit is contained in:
2026-08-27 12:32:20 +02:00
parent d44c24ec97
commit edb6e5e027
97 changed files with 9535 additions and 195 deletions

View File

@@ -6,29 +6,93 @@ from typing import Any
import pytest
from ruamel.yaml import YAML
#: Half a second of silence, so ``mutagen`` reports a real duration and tags can be
#: written onto a file that is actually an MP3. Generated once with ffmpeg.
SILENCE = Path(__file__).parent / "data" / "silence.mp3"
VALID_CONFIG: dict[str, Any] = {
"general": {
"figure_folder": "music",
"library": {"root": "music", "cache": ".cache"},
"serial_port": "/dev/ttyUSB0",
"alsa_device": "simulate",
"min_volume": 0,
"max_volume": 60,
"initial_volume": 40,
},
"figures": {
"fuchs": {"id": "04a1b2c3d4", "colors": ["#ff6600", "#ffcc00", "#331100", "wff"]},
"eule": {"id": "04b2c3d4e5", "colors": ["#3355ff", "#66aaff", "#001133", "#ffffff"]},
# One of each, so the figure-kind branch is exercised by every fixture.
"eule": {
"id": "04b2c3d4e5",
"colors": ["#3355ff", "#66aaff", "#001133", "#ffffff"],
"kind": "book",
},
},
}
def write_track(path: Path, **tags: str) -> Path:
"""Copy the silence fixture to ``path`` and stamp the given easy-mode ID3 tags."""
import mutagen
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(SILENCE.read_bytes())
if tags:
audio = mutagen.File(path, easy=True)
if audio.tags is None:
audio.add_tags()
for key, value in tags.items():
audio[key] = value
audio.save()
return path
@pytest.fixture
def config_dir(tmp_path: Path) -> Path:
"""A directory with a valid ``music/`` tree and two figures' worth of tracks."""
"""A directory holding a small but structurally real music library.
Two figure folders (matching ``VALID_CONFIG``), one music album, one audiobook and
one podcast - enough for every branch the scanner has.
"""
root = tmp_path / "music"
for figure, tracks in (("fuchs", 3), ("eule", 2)):
folder = tmp_path / "music" / figure
folder.mkdir(parents=True)
for index in range(tracks):
(folder / f"{index:02d} - track.mp3").write_bytes(b"")
write_track(
root / "Figuren" / figure / f"{index:02d} - track.mp3",
title=f"Track {index}",
album="Waldlieder",
albumartist="Die Tiere",
)
for index in range(2):
write_track(
root / "Musik" / "Kinderparty - Kinderparty Lieder" / f"{index:02d} - lied.mp3",
title=f"Lied {index}",
album="Kinderparty Lieder",
albumartist="Kinderparty",
)
for index in range(2):
write_track(
root / "Hörbücher" / "Conni - Conni in den Bergen" / f"{index:02d} - teil.mp3",
title=f"Teil {index}",
album="Conni in den Bergen",
albumartist="Conni, Julia Boehme",
)
podcast = root / "Kinderpodcasts" / "Wissen macht Ah"
for date, title in (("20240101", "Alt"), ("20260101", "Neu")):
write_track(
podcast / f"{date} - {title}.mp3",
title=title,
album="Wissen macht Ah! - Podcast",
albumartist="Ein Name, Noch Einer, Und Einer",
)
# The two kinds of scratch file a podcast downloader leaves behind.
(podcast / "archive.json").write_text("[]")
(podcast / ".podcast-dl-abc.download.tmp").write_bytes(b"")
return tmp_path