diff --git a/app/api/v1/tracks.py b/app/api/v1/tracks.py index 39e2e8e..8c24e41 100644 --- a/app/api/v1/tracks.py +++ b/app/api/v1/tracks.py @@ -71,6 +71,7 @@ async def list_tracks( artist_id: uuid.UUID | None = None, album_id: uuid.UUID | 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)$"), order: str = Query("desc", pattern="^(asc|desc)$"), limit: int = Query(50, ge=1, le=200), @@ -80,12 +81,15 @@ async def list_tracks( artist_id=artist_id, album_id=album_id, q=q, + source=source, sort_by=sort_by, order=order, limit=limit, 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}) album_ids = list({t.album_id for t in tracks if t.album_id is not None}) diff --git a/app/domain/ports.py b/app/domain/ports.py index 0ce8192..977a75d 100644 --- a/app/domain/ports.py +++ b/app/domain/ports.py @@ -145,6 +145,7 @@ class TrackRepository(Protocol): artist_id: uuid.UUID | None, album_id: uuid.UUID | None, q: str | None, + source: str | None = None, sort_by: str, order: str, limit: int, @@ -156,6 +157,7 @@ class TrackRepository(Protocol): artist_id: uuid.UUID | None, album_id: uuid.UUID | None, q: str | None, + source: str | None = None, ) -> int: ... async def update( self, diff --git a/app/infrastructure/db/repositories/track_repository.py b/app/infrastructure/db/repositories/track_repository.py index 72ec7cb..d76c1e6 100644 --- a/app/infrastructure/db/repositories/track_repository.py +++ b/app/infrastructure/db/repositories/track_repository.py @@ -170,6 +170,7 @@ class SqlAlchemyTrackRepository: artist_id: uuid.UUID | None, album_id: uuid.UUID | None, q: str | None, + source: str | None = None, sort_by: str = "created_at", order: str = "desc", limit: int = 50, @@ -180,6 +181,8 @@ class SqlAlchemyTrackRepository: stmt = stmt.where(TrackModel.artist_id == artist_id) if album_id is not None: stmt = stmt.where(TrackModel.album_id == album_id) + if source is not None: + stmt = stmt.where(TrackModel.source == source) if q: stmt = stmt.where(TrackModel.title.ilike(f"%{q}%")) @@ -204,12 +207,15 @@ class SqlAlchemyTrackRepository: artist_id: uuid.UUID | None, album_id: uuid.UUID | None, q: str | None, + source: str | None = None, ) -> int: stmt = select(func.count()).select_from(TrackModel) if artist_id is not None: stmt = stmt.where(TrackModel.artist_id == artist_id) if album_id is not None: stmt = stmt.where(TrackModel.album_id == album_id) + if source is not None: + stmt = stmt.where(TrackModel.source == source) if q: stmt = stmt.where(TrackModel.title.ilike(f"%{q}%")) return (await self._session.execute(stmt)).scalar_one() diff --git a/tests/test_upload_stream_api.py b/tests/test_upload_stream_api.py index f87f88c..2687d59 100644 --- a/tests/test_upload_stream_api.py +++ b/tests/test_upload_stream_api.py @@ -190,3 +190,25 @@ async def test_upload_requires_auth(api: AsyncClient) -> None: files={"file": ("x.mp3", b"data", "audio/mpeg")}, ) 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"] == []