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>
86 lines
2.4 KiB
Python
86 lines
2.4 KiB
Python
"""Storage analysis and cleanup endpoints."""
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter
|
|
|
|
from app.api.deps import (
|
|
AlbumRepoDep,
|
|
ArtistRepoDep,
|
|
CurrentUser,
|
|
FileStorageDep,
|
|
TrackRepoDep,
|
|
)
|
|
from app.api.schemas.storage import (
|
|
DiskUsageOut,
|
|
FormatBreakdownOut,
|
|
GenreCountOut,
|
|
StorageStatsOut,
|
|
)
|
|
|
|
router = APIRouter(prefix="/storage", tags=["storage"])
|
|
|
|
# How many of the most common genres the dashboard surfaces.
|
|
_TOP_GENRES = 8
|
|
|
|
|
|
@router.get("")
|
|
async def get_storage_stats(
|
|
track_repo: TrackRepoDep,
|
|
artist_repo: ArtistRepoDep,
|
|
album_repo: AlbumRepoDep,
|
|
storage: FileStorageDep,
|
|
_: CurrentUser,
|
|
) -> StorageStatsOut:
|
|
"""Library + disk statistics for the Storage dashboard (§A6).
|
|
|
|
Aggregates come from the catalogue (cheap GROUP BYs); ``disk`` reflects the
|
|
real backing volume and is ``None`` for backends without a fixed-capacity
|
|
disk (e.g. object stores)."""
|
|
stats = await track_repo.library_stats()
|
|
total_artists = await artist_repo.count(q=None)
|
|
total_albums = await album_repo.count(artist_id=None, q=None)
|
|
genres = await track_repo.genres()
|
|
disk = await storage.disk_usage()
|
|
|
|
return StorageStatsOut(
|
|
total_tracks=stats.total_tracks,
|
|
total_artists=total_artists,
|
|
total_albums=total_albums,
|
|
total_size=stats.total_size,
|
|
total_duration_seconds=stats.total_duration_seconds,
|
|
largest_track_size=stats.largest_track_size,
|
|
earliest_added=stats.earliest_added,
|
|
latest_added=stats.latest_added,
|
|
by_format=[
|
|
FormatBreakdownOut(
|
|
file_format=f.file_format,
|
|
track_count=f.track_count,
|
|
total_size=f.total_size,
|
|
)
|
|
for f in stats.by_format
|
|
],
|
|
by_metadata_status=stats.by_metadata_status,
|
|
by_source=stats.by_source,
|
|
top_genres=[
|
|
GenreCountOut(genre=genre, track_count=count) for genre, count in genres[:_TOP_GENRES]
|
|
],
|
|
disk=DiskUsageOut(total=disk.total, used=disk.used, free=disk.free) if disk else None,
|
|
)
|
|
|
|
|
|
@router.get("/duplicates")
|
|
async def get_duplicates() -> Any: ...
|
|
|
|
|
|
@router.get("/broken")
|
|
async def get_broken_files() -> Any: ...
|
|
|
|
|
|
@router.get("/missing-metadata")
|
|
async def get_missing_metadata() -> Any: ...
|
|
|
|
|
|
@router.post("/cleanup")
|
|
async def run_cleanup() -> Any: ...
|