feat: cover-art pipeline (§1D)
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>
This commit is contained in:
@@ -19,6 +19,8 @@ from app.infrastructure.db.repositories import (
|
||||
SqlAlchemyTrackRepository,
|
||||
)
|
||||
from app.infrastructure.metadata.acoustid import AcoustIdHttpClient
|
||||
from app.infrastructure.metadata.cover_extractor import MutagenCoverExtractor
|
||||
from app.infrastructure.metadata.coverart import CoverArtArchiveClient
|
||||
from app.infrastructure.metadata.fingerprint import FpcalcFingerprinter
|
||||
from app.infrastructure.metadata.tags import MutagenTagReader
|
||||
from app.infrastructure.storage.provider import get_file_storage
|
||||
@@ -28,14 +30,17 @@ log = get_logger("worker.enrich")
|
||||
|
||||
async def enrich_track(_ctx: dict[str, Any], *, track_id: str) -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
api_key = (
|
||||
settings.acoustid_api_key.get_secret_value() if settings.acoustid_api_key else None
|
||||
)
|
||||
api_key = settings.acoustid_api_key.get_secret_value() if settings.acoustid_api_key else None
|
||||
acoustid = AcoustIdHttpClient(
|
||||
api_key=api_key,
|
||||
user_agent=settings.musicbrainz_user_agent,
|
||||
api_url=settings.acoustid_api_url,
|
||||
)
|
||||
cover_provider = CoverArtArchiveClient(
|
||||
user_agent=settings.musicbrainz_user_agent,
|
||||
enabled=settings.coverart_enabled,
|
||||
base_url=settings.coverart_base_url,
|
||||
)
|
||||
|
||||
async with session_scope() as session:
|
||||
service = MetadataEnrichmentService(
|
||||
@@ -46,6 +51,8 @@ async def enrich_track(_ctx: dict[str, Any], *, track_id: str) -> dict[str, Any]
|
||||
tag_reader=MutagenTagReader(),
|
||||
fingerprinter=FpcalcFingerprinter(settings.fpcalc_path),
|
||||
acoustid=acoustid,
|
||||
cover_extractor=MutagenCoverExtractor(),
|
||||
cover_provider=cover_provider,
|
||||
)
|
||||
result = await service.enrich(uuid.UUID(track_id))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user