diff --git a/backend/shonar/api/v1/recordings.py b/backend/shonar/api/v1/recordings.py index acc7db9..d3b0255 100644 --- a/backend/shonar/api/v1/recordings.py +++ b/backend/shonar/api/v1/recordings.py @@ -6,6 +6,7 @@ return 404 (no existence leaks). Storage keys are never exposed. from __future__ import annotations +import contextlib import uuid from fastapi import APIRouter, Header, HTTPException, Query, Request, Response @@ -141,7 +142,10 @@ async def list_recordings( session: SessionDep, limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), - sort: str = Query(default="recorded_at", pattern="^(recorded_at|created_at|duration_seconds|title)$"), + sort: str = Query( + default="recorded_at", + pattern="^(recorded_at|created_at|duration_seconds|title)$", + ), order: str = Query(default="desc", pattern="^(asc|desc)$"), ): where = [Recording.user_id == user.id, Recording.deleted_at.is_(None)] @@ -228,10 +232,8 @@ async def delete_recording( await session.delete(rec) # cascades to assets/transcripts/summaries/jobs await session.flush() for a in assets: - try: + with contextlib.suppress(Exception): # best effort await storage.delete(a.storage_key) - except Exception: # pragma: no cover - best effort - pass return Response(status_code=204) @@ -250,7 +252,8 @@ async def download_audio(recording_id: uuid.UUID, user: CurrentUser, session: Se from shonar.storage import get_storage data = await get_storage().get(original.storage_key) - filename = f"{rec.recorded_at:%Y%m%d-%H%M%S}{original.storage_key[original.storage_key.rfind('.'):]}" + ext = original.storage_key[original.storage_key.rfind(".") :] + filename = f"{rec.recorded_at:%Y%m%d-%H%M%S}{ext}" return RawResponse( content=data, media_type=original.mime_type, diff --git a/backend/tests/test_recordings.py b/backend/tests/test_recordings.py index 628d5f4..d970bbc 100644 --- a/backend/tests/test_recordings.py +++ b/backend/tests/test_recordings.py @@ -39,7 +39,8 @@ async def auth(token: str) -> dict: return {"Authorization": f"Bearer {token}"} -async def upload_full(client, token: str, data: bytes, mime="audio/wav", client_id=None, title=None): +async def upload_full(client, token: str, data: bytes, mime="audio/wav", + client_id=None, title=None): h = await auth(token) r = await client.post( "/api/v1/uploads", @@ -114,7 +115,8 @@ async def test_finalize_rejects_size_mismatch(client): headers={**h, "content-type": "application/octet-stream"}) r = await client.post(f"/api/v1/uploads/{sid}/finalize", json={}, headers=h) assert r.status_code == 422 - assert "missing chunks" in r.json()["detail"].lower() or "size mismatch" in r.json()["detail"].lower() + detail = r.json()["detail"].lower() + assert "missing chunks" in detail or "size mismatch" in detail async def test_chunk_resume_status_and_idempotency(client): diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 320d5f5..65c7d1f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -8,7 +8,7 @@ updated in the same commit as the work it describes. |---|---|---| | M0 | Repo scaffold, license, docs, Docker dev stack, `/healthz` `/readyz`, Alembic schema | done | | M1 | Auth: register / login / rotating refresh + reuse detection / logout / delete-account, Argon2id, rate limits | done | -| M2 | Upload sessions (chunked, resumable), storage abstraction (local + S3), recordings CRUD, ownership checks | in progress | +| M2 | Upload sessions (chunked, resumable), storage abstraction (local + S3), recordings CRUD, ownership checks | done | | M3 | Android: server URL config, login, token persistence + auto-refresh | TODO | | M4 | Android: foreground-service recording (pause/resume/stop), metadata, Room | TODO | | M5 | Android: WorkManager upload sync (retry, Wi-Fi-only, charging-only, pause) | TODO |