78007461e1
Pluggable fetch source: ytmusicapi search + yt-dlp download (cookies-file guard), DownloadJob entity/repo + DownloadService, download_task worker with exponential-backoff retries, and wired /search, /sources/{source}/search, and /downloads endpoints. Adds youtube_enabled/cookies config, yt-dlp+ytmusicapi deps, and the download_jobs.track_id migration. Snapshot also bundles in-progress storage/tracks/acoustid edits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.3 KiB
Python
79 lines
2.3 KiB
Python
"""Download job endpoints (§A5). Heavy work is dispatched to arq workers — these
|
|
handlers only create/inspect/cancel/retry job records."""
|
|
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Query, Response
|
|
|
|
from app.api.deps import CurrentUser, DownloadServiceDep
|
|
from app.api.schemas.download import DownloadCreate, DownloadCreateResponse, DownloadJobOut
|
|
from app.api.schemas.pagination import PagedResponse
|
|
|
|
router = APIRouter(prefix="/downloads", tags=["downloads"])
|
|
|
|
|
|
@router.get("")
|
|
async def list_downloads(
|
|
service: DownloadServiceDep,
|
|
user: CurrentUser,
|
|
status: str | None = Query(default=None),
|
|
mine: bool = Query(default=False),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
offset: int = Query(0, ge=0),
|
|
) -> PagedResponse[DownloadJobOut]:
|
|
jobs, total = await service.list(
|
|
requested_by=user.id if mine else None,
|
|
status=status,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
return PagedResponse(
|
|
items=[DownloadJobOut.from_entity(j) for j in jobs],
|
|
total=total,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
|
|
@router.post("", status_code=202)
|
|
async def create_download(
|
|
body: DownloadCreate,
|
|
service: DownloadServiceDep,
|
|
user: CurrentUser,
|
|
) -> DownloadCreateResponse:
|
|
result = await service.request(
|
|
source=body.source,
|
|
source_id=body.source_id,
|
|
query=body.query,
|
|
requested_by=user.id,
|
|
)
|
|
return DownloadCreateResponse(
|
|
already_in_library=result.already_in_library,
|
|
track_id=result.track_id,
|
|
job=DownloadJobOut.from_entity(result.job) if result.job is not None else None,
|
|
)
|
|
|
|
|
|
@router.get("/{job_id}")
|
|
async def get_download(
|
|
job_id: uuid.UUID, service: DownloadServiceDep, _: CurrentUser
|
|
) -> DownloadJobOut:
|
|
job = await service.get(job_id)
|
|
return DownloadJobOut.from_entity(job)
|
|
|
|
|
|
@router.delete("/{job_id}", status_code=204)
|
|
async def cancel_download(
|
|
job_id: uuid.UUID, service: DownloadServiceDep, _: CurrentUser
|
|
) -> Response:
|
|
await service.cancel(job_id)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.post("/{job_id}/retry")
|
|
async def retry_download(
|
|
job_id: uuid.UUID, service: DownloadServiceDep, _: CurrentUser
|
|
) -> DownloadJobOut:
|
|
job = await service.retry(job_id)
|
|
return DownloadJobOut.from_entity(job)
|