feat(tracks): filter track list by ingest source
Add an optional `source` filter to `GET /api/v1/tracks` (and the `TrackRepository.list`/`count` port + SQLAlchemy adapter). Lets clients query, e.g., only uploaded tracks (`?source=upload`) newest-first — the backing for the webui's persistent "Recently uploaded" view. - test: upload then list with `?source=upload` (hit) / `?source=youtube` (miss) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,7 @@ async def list_tracks(
|
|||||||
artist_id: uuid.UUID | None = None,
|
artist_id: uuid.UUID | None = None,
|
||||||
album_id: uuid.UUID | None = None,
|
album_id: uuid.UUID | None = None,
|
||||||
q: str | None = None,
|
q: str | None = None,
|
||||||
|
source: str | None = Query(None, max_length=32),
|
||||||
sort_by: str = Query("created_at", pattern="^(title|created_at|artist)$"),
|
sort_by: str = Query("created_at", pattern="^(title|created_at|artist)$"),
|
||||||
order: str = Query("desc", pattern="^(asc|desc)$"),
|
order: str = Query("desc", pattern="^(asc|desc)$"),
|
||||||
limit: int = Query(50, ge=1, le=200),
|
limit: int = Query(50, ge=1, le=200),
|
||||||
@@ -80,12 +81,15 @@ async def list_tracks(
|
|||||||
artist_id=artist_id,
|
artist_id=artist_id,
|
||||||
album_id=album_id,
|
album_id=album_id,
|
||||||
q=q,
|
q=q,
|
||||||
|
source=source,
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
order=order,
|
order=order,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
total = await track_repo.count(artist_id=artist_id, album_id=album_id, q=q)
|
total = await track_repo.count(
|
||||||
|
artist_id=artist_id, album_id=album_id, q=q, source=source
|
||||||
|
)
|
||||||
|
|
||||||
artist_ids = list({t.artist_id for t in tracks})
|
artist_ids = list({t.artist_id for t in tracks})
|
||||||
album_ids = list({t.album_id for t in tracks if t.album_id is not None})
|
album_ids = list({t.album_id for t in tracks if t.album_id is not None})
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ class TrackRepository(Protocol):
|
|||||||
artist_id: uuid.UUID | None,
|
artist_id: uuid.UUID | None,
|
||||||
album_id: uuid.UUID | None,
|
album_id: uuid.UUID | None,
|
||||||
q: str | None,
|
q: str | None,
|
||||||
|
source: str | None = None,
|
||||||
sort_by: str,
|
sort_by: str,
|
||||||
order: str,
|
order: str,
|
||||||
limit: int,
|
limit: int,
|
||||||
@@ -156,6 +157,7 @@ class TrackRepository(Protocol):
|
|||||||
artist_id: uuid.UUID | None,
|
artist_id: uuid.UUID | None,
|
||||||
album_id: uuid.UUID | None,
|
album_id: uuid.UUID | None,
|
||||||
q: str | None,
|
q: str | None,
|
||||||
|
source: str | None = None,
|
||||||
) -> int: ...
|
) -> int: ...
|
||||||
async def update(
|
async def update(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class SqlAlchemyTrackRepository:
|
|||||||
artist_id: uuid.UUID | None,
|
artist_id: uuid.UUID | None,
|
||||||
album_id: uuid.UUID | None,
|
album_id: uuid.UUID | None,
|
||||||
q: str | None,
|
q: str | None,
|
||||||
|
source: str | None = None,
|
||||||
sort_by: str = "created_at",
|
sort_by: str = "created_at",
|
||||||
order: str = "desc",
|
order: str = "desc",
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
@@ -180,6 +181,8 @@ class SqlAlchemyTrackRepository:
|
|||||||
stmt = stmt.where(TrackModel.artist_id == artist_id)
|
stmt = stmt.where(TrackModel.artist_id == artist_id)
|
||||||
if album_id is not None:
|
if album_id is not None:
|
||||||
stmt = stmt.where(TrackModel.album_id == album_id)
|
stmt = stmt.where(TrackModel.album_id == album_id)
|
||||||
|
if source is not None:
|
||||||
|
stmt = stmt.where(TrackModel.source == source)
|
||||||
if q:
|
if q:
|
||||||
stmt = stmt.where(TrackModel.title.ilike(f"%{q}%"))
|
stmt = stmt.where(TrackModel.title.ilike(f"%{q}%"))
|
||||||
|
|
||||||
@@ -204,12 +207,15 @@ class SqlAlchemyTrackRepository:
|
|||||||
artist_id: uuid.UUID | None,
|
artist_id: uuid.UUID | None,
|
||||||
album_id: uuid.UUID | None,
|
album_id: uuid.UUID | None,
|
||||||
q: str | None,
|
q: str | None,
|
||||||
|
source: str | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
stmt = select(func.count()).select_from(TrackModel)
|
stmt = select(func.count()).select_from(TrackModel)
|
||||||
if artist_id is not None:
|
if artist_id is not None:
|
||||||
stmt = stmt.where(TrackModel.artist_id == artist_id)
|
stmt = stmt.where(TrackModel.artist_id == artist_id)
|
||||||
if album_id is not None:
|
if album_id is not None:
|
||||||
stmt = stmt.where(TrackModel.album_id == album_id)
|
stmt = stmt.where(TrackModel.album_id == album_id)
|
||||||
|
if source is not None:
|
||||||
|
stmt = stmt.where(TrackModel.source == source)
|
||||||
if q:
|
if q:
|
||||||
stmt = stmt.where(TrackModel.title.ilike(f"%{q}%"))
|
stmt = stmt.where(TrackModel.title.ilike(f"%{q}%"))
|
||||||
return (await self._session.execute(stmt)).scalar_one()
|
return (await self._session.execute(stmt)).scalar_one()
|
||||||
|
|||||||
@@ -190,3 +190,25 @@ async def test_upload_requires_auth(api: AsyncClient) -> None:
|
|||||||
files={"file": ("x.mp3", b"data", "audio/mpeg")},
|
files={"file": ("x.mp3", b"data", "audio/mpeg")},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 401
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_tracks_filters_by_source(api: AsyncClient) -> None:
|
||||||
|
# Uploaded tracks carry source="upload"; `?source=` narrows the list (this
|
||||||
|
# powers the webui's "Recently uploaded" view, which survives a refresh).
|
||||||
|
token = await _login(api)
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
await api.post(
|
||||||
|
"/api/v1/upload",
|
||||||
|
files={"file": ("uploaded.mp3", b"upload bytes" * 80, "audio/mpeg")},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
hit = await api.get("/api/v1/tracks", params={"source": "upload"}, headers=headers)
|
||||||
|
assert hit.status_code == 200, hit.text
|
||||||
|
items = hit.json()["items"]
|
||||||
|
assert len(items) == 1
|
||||||
|
assert items[0]["source"] == "upload"
|
||||||
|
|
||||||
|
miss = await api.get("/api/v1/tracks", params={"source": "youtube"}, headers=headers)
|
||||||
|
assert miss.status_code == 200
|
||||||
|
assert miss.json()["items"] == []
|
||||||
|
|||||||
Reference in New Issue
Block a user