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>
64 lines
2.4 KiB
Python
64 lines
2.4 KiB
Python
"""arq task: enrich one track's metadata (plan §6.2, §1D).
|
|
|
|
Wires the §6.2 pipeline adapters to :class:`MetadataEnrichmentService` and runs
|
|
it in the worker's own transactional session. Enqueued (deferred) after upload
|
|
and after a local-folder import. Idempotent and best-effort — a missing track or
|
|
a ``manual`` one is a clean no-op.
|
|
"""
|
|
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from app.application.metadata_service import MetadataEnrichmentService
|
|
from app.core.config import get_settings
|
|
from app.core.logging import get_logger
|
|
from app.infrastructure.db import session_scope
|
|
from app.infrastructure.db.repositories import (
|
|
SqlAlchemyAlbumRepository,
|
|
SqlAlchemyArtistRepository,
|
|
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
|
|
|
|
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
|
|
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(
|
|
tracks=SqlAlchemyTrackRepository(session),
|
|
artists=SqlAlchemyArtistRepository(session),
|
|
albums=SqlAlchemyAlbumRepository(session),
|
|
storage=get_file_storage(),
|
|
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))
|
|
|
|
return {
|
|
"track_id": str(result.track_id),
|
|
"status": result.status,
|
|
"mbid": result.matched_mbid,
|
|
}
|