feat(sources): YouTube Music search + download pipeline (§1C/§1E)
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>
This commit is contained in:
+58
-2
@@ -10,8 +10,14 @@ here — a source yields a file plus a minimal title; enrichment (plan §6.2) fi
|
||||
the rest later, so this stays a thin discovery layer (CLAUDE.md: no duplicated
|
||||
business logic)."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# A source's ``kind`` describes which ports it satisfies, so the UI/admin can
|
||||
# tell an indexed folder from a searchable fetch-source. A backend may be both.
|
||||
KIND_INDEXABLE = "indexable" # enumerates files already on disk (local folder)
|
||||
KIND_FETCH = "fetch" # searches + downloads from an external service (YTM, …)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -20,7 +26,7 @@ class SourceInfo:
|
||||
|
||||
name: str
|
||||
label: str
|
||||
kind: str # "indexable" (more kinds — search/download — arrive with youtube)
|
||||
kind: str # KIND_INDEXABLE | KIND_FETCH
|
||||
available: bool
|
||||
|
||||
|
||||
@@ -37,3 +43,53 @@ class SourceFile:
|
||||
suggested_title: str
|
||||
file_format: str
|
||||
file_size: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SearchResult:
|
||||
"""One hit from a searchable source (plan §5), shown on the discover screen.
|
||||
|
||||
``source_id`` is the stable handle the same backend later resolves in
|
||||
``fetch`` — it must round-trip a download request without re-searching.
|
||||
``raw`` carries the backend's untouched payload for debugging / future use.
|
||||
"""
|
||||
|
||||
source: str
|
||||
source_id: str
|
||||
title: str
|
||||
artist: str | None
|
||||
album: str | None
|
||||
duration_seconds: int | None
|
||||
thumbnail_url: str | None
|
||||
raw: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RawMetadata:
|
||||
"""Metadata a fetch-source can offer about an item *before* enrichment.
|
||||
|
||||
Best-effort and source-shaped — the canonical metadata still comes from the
|
||||
enrichment pipeline (plan §6.2). Used to seed a more useful provisional
|
||||
title than a bare id while a download is queued."""
|
||||
|
||||
title: str | None
|
||||
artist: str | None
|
||||
album: str | None
|
||||
year: int | None
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DownloadResult:
|
||||
"""A file a fetch-source produced on local disk (plan §5).
|
||||
|
||||
``path`` is a temp file the caller owns: it is stored into managed storage
|
||||
and then removed (same lifecycle as an upload). ``source_id`` is echoed back
|
||||
because some backends only learn the canonical id during the download."""
|
||||
|
||||
source_id: str
|
||||
path: Path
|
||||
file_format: str
|
||||
file_size: int
|
||||
bitrate: int | None
|
||||
suggested_title: str
|
||||
|
||||
Reference in New Issue
Block a user