feat(storage): library + disk statistics endpoint (§A6)
Docker Build & Publish / build (push) Has been cancelled
Docker Build & Publish / push (push) Has been cancelled
Docker Build & Publish / Prune old image versions (push) Has been cancelled

Implement `GET /api/v1/storage`, replacing the stub. Returns aggregate
library facts (track/artist/album counts, total footprint, playtime,
per-format / per-source / metadata-status breakdowns, top genres) plus
the real capacity of the backing volume.

- domain: `LibraryStats`, `FormatBreakdown`, `DiskUsage` value objects
- ports: `FileStorage.disk_usage()` (local = shutil.disk_usage walking up
  to the nearest existing ancestor; S3 returns None — no fixed disk)
- repo: `TrackRepository.library_stats()` (single set of GROUP BYs)
- tests: storage stats API (auth, empty library, upload counting)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Senko-san
2026-06-14 01:19:53 +03:00
parent 636820afb8
commit fa23568214
9 changed files with 371 additions and 5 deletions
+10 -2
View File
@@ -17,7 +17,9 @@ from app.domain.entities import (
AudioTags,
CoverArt,
Credentials,
DiskUsage,
Fingerprint,
LibraryStats,
Like,
ObjectStat,
PlayHistoryEntry,
@@ -98,6 +100,10 @@ class FileStorage(Protocol):
async def exists(self, key: str) -> bool: ...
async def delete(self, key: str) -> None: ...
def as_local_path(self, key: str) -> AbstractAsyncContextManager[Path]: ...
async def disk_usage(self) -> DiskUsage | None:
"""Capacity of the volume backing the store, or ``None`` when the
backend has no addressable disk (e.g. an object store)."""
...
class ArtistRepository(Protocol):
@@ -128,9 +134,11 @@ class TrackRepository(Protocol):
added_by: uuid.UUID | None,
) -> Track: ...
async def delete(self, track_id: uuid.UUID) -> None: ...
# genres must come before ``list`` — the method named ``list`` shadows the
# builtin in later annotations (same pattern as AlbumRepository below).
# genres / library_stats must come before ``list`` — the method named
# ``list`` shadows the builtin in later annotations (same pattern as
# AlbumRepository below).
async def genres(self) -> list[tuple[str, int]]: ...
async def library_stats(self) -> LibraryStats: ...
async def list(
self,
*,