0bb752f582
Resolve, store and serve album cover art.
Sources (tag-first, mirroring enrichment): embedded artwork extracted
offline via mutagen (ID3 APIC / FLAC+OGG Picture / MP4 covr), then Cover
Art Archive by release-group MBID as a network fallback. Resolution runs
inside MetadataEnrichmentService after album resolution, only when the
album has no cover yet (idempotent, never overwrites), and is best-effort
so a cover failure never affects enrichment status.
- CoverArt value object + CoverArtExtractor/CoverArtProvider ports
- MutagenCoverExtractor + CoverArtArchiveClient adapters
- AcoustID parser now captures release_group_mbid
- Covers stored via FileStorage at covers/{album_id}.{ext} (local + S3)
- AlbumRepository.set_cover_path
- Serve real covers: GET /api/v1/albums|tracks/{id}/cover (StreamUser,
?token=), Subsonic getCoverArt (placeholder fallback)
- has_cover flag on AlbumOut/TrackOut
- coverart_enabled / coverart_base_url settings
- tests: cover resolution units + release_group parse + DB-backed
test_cover_api.py (139 green via make test-api)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
1.7 KiB
Python
55 lines
1.7 KiB
Python
"""Value objects for the metadata-enrichment pipeline (plan §6.2).
|
|
|
|
Pure data carriers between the enrichment service and its adapters (tag reader,
|
|
fingerprinter, AcoustID). No framework imports — these cross the domain boundary.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AudioTags:
|
|
"""Embedded tags read from the file itself (ID3 / Vorbis / MP4 …).
|
|
|
|
Every field is optional — files are tagged inconsistently. The reader fills
|
|
what it can and leaves the rest ``None`` for downstream identification.
|
|
"""
|
|
|
|
title: str | None = None
|
|
artist: str | None = None
|
|
album: str | None = None
|
|
album_artist: str | None = None
|
|
genre: str | None = None
|
|
year: int | None = None
|
|
track_number: int | None = None
|
|
duration_seconds: int | None = None
|
|
bitrate: int | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Fingerprint:
|
|
"""Chromaprint fingerprint plus the decoded duration (both needed by AcoustID)."""
|
|
|
|
fingerprint: str
|
|
duration_seconds: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RecordingMatch:
|
|
"""A single AcoustID result, flattened to the fields enrichment cares about.
|
|
|
|
``acoustid`` is the stable AcoustID identifier (a UUID) — used as the
|
|
dedup key persisted on ``track.acoustid_fingerprint`` (fits the 64-char
|
|
column; the raw fingerprint does not). ``recording_mbid`` is the MusicBrainz
|
|
recording id when present.
|
|
"""
|
|
|
|
acoustid: str
|
|
score: float
|
|
recording_mbid: str | None = None
|
|
release_group_mbid: str | None = None
|
|
title: str | None = None
|
|
artist: str | None = None
|
|
album: str | None = None
|
|
year: int | None = None
|