e45e578f54
Search results now report whether a hit is already saved (in_library,
track_id, availability). New RemoteLibraryService backs POST
/tracks/remote (idempotent placeholder save) and POST
/tracks/{id}/materialize (on-demand fetch via a new materialize_track
arq task, reusing in-flight jobs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
"""arq task: materialize a remote placeholder track (plan: Model C).
|
|
|
|
Counterpart to ``download_task`` for tracks that were *saved* from a remote
|
|
browse hit without audio (``availability="remote"``, ``storage_uri=NULL``).
|
|
The job's ``track_id`` already points at the existing placeholder row — on
|
|
success the file is stored and ``TrackRepository.materialize`` fills the row
|
|
in place (the track's ``id`` never changes), then enrichment is enqueued as
|
|
usual.
|
|
|
|
Shares its fetch/retry/failure machinery with ``download_task`` — only the
|
|
"what happens on success" step differs (fill in an existing row vs. create a
|
|
new one).
|
|
"""
|
|
|
|
import contextlib
|
|
import uuid
|
|
from typing import Any
|
|
|
|
import anyio
|
|
|
|
from app.core.config import get_settings
|
|
from app.core.logging import correlation_id, get_logger
|
|
from app.domain.errors import NotFoundError, ValidationError
|
|
from app.domain.sources import DownloadResult
|
|
from app.infrastructure.db import session_scope
|
|
from app.infrastructure.db.repositories import (
|
|
SqlAlchemyDownloadJobRepository,
|
|
SqlAlchemyTrackRepository,
|
|
)
|
|
from app.infrastructure.sources.registry import build_source_registry
|
|
from app.infrastructure.storage.provider import get_file_storage
|
|
from app.workers.queue import enqueue_enrich
|
|
from app.workers.tasks.download_task import _handle_failure, _load_job, _mark_failed, _run_fetch
|
|
|
|
log = get_logger("worker.materialize")
|
|
|
|
|
|
async def materialize_track(_ctx: dict[str, Any], *, job_id: str) -> dict[str, Any]:
|
|
correlation_id.set(f"mat:{job_id}")
|
|
jid = uuid.UUID(job_id)
|
|
settings = get_settings()
|
|
|
|
job = await _load_job(jid)
|
|
if job is None:
|
|
log.info("materialize_job_missing", job_id=job_id) # cancelled before pickup
|
|
return {"job_id": job_id, "status": "missing"}
|
|
if job.track_id is None or job.source_id is None:
|
|
await _mark_failed(jid, "Materialize job missing track_id/source_id.")
|
|
return {"job_id": job_id, "status": "failed"}
|
|
|
|
registry = build_source_registry(settings)
|
|
try:
|
|
backend = registry.fetchable(job.source)
|
|
except (NotFoundError, ValidationError) as exc:
|
|
await _mark_failed(jid, f"Source unavailable: {exc}")
|
|
return {"job_id": job_id, "status": "failed"}
|
|
|
|
await _set_status(jid, "downloading")
|
|
try:
|
|
result = await _run_fetch(backend, job.source_id, jid)
|
|
except Exception as exc:
|
|
return await _handle_failure(jid, exc, settings.download_max_retries, job_id)
|
|
|
|
try:
|
|
await _materialize_result(jid, job.track_id, result)
|
|
except Exception as exc:
|
|
log.exception("materialize_finalize_failed", job_id=job_id)
|
|
await _mark_failed(jid, f"Materialize failed: {type(exc).__name__}: {exc}")
|
|
return {"job_id": job_id, "status": "failed"}
|
|
|
|
await enqueue_enrich(job.track_id)
|
|
log.info("materialize_complete", job_id=job_id, track_id=str(job.track_id))
|
|
return {"job_id": job_id, "status": "done", "track_id": str(job.track_id)}
|
|
|
|
|
|
async def _materialize_result(jid: uuid.UUID, track_id: uuid.UUID, result: DownloadResult) -> None:
|
|
"""Store the downloaded file and fill in the placeholder track in place."""
|
|
key = f"tracks/{str(track_id)[:2]}/{track_id}.{result.file_format}"
|
|
storage = get_file_storage()
|
|
try:
|
|
await storage.save_file(key, result.path)
|
|
async with session_scope() as session:
|
|
job_repo = SqlAlchemyDownloadJobRepository(session)
|
|
await job_repo.set_status(jid, status="enriching")
|
|
tracks = SqlAlchemyTrackRepository(session)
|
|
await tracks.materialize(
|
|
track_id,
|
|
storage_uri=key,
|
|
file_format=result.file_format,
|
|
file_size=result.file_size,
|
|
bitrate=result.bitrate,
|
|
)
|
|
await job_repo.set_status(jid, status="done", track_id=track_id)
|
|
finally:
|
|
with contextlib.suppress(Exception):
|
|
await anyio.Path(result.path).unlink(missing_ok=True)
|
|
|
|
|
|
async def _set_status(jid: uuid.UUID, status: str) -> None:
|
|
async with session_scope() as session:
|
|
await SqlAlchemyDownloadJobRepository(session).set_status(jid, status=status)
|