"""Client sync endpoints (offline-first event log). ``GET /sync/changes`` pulls everything the caller changed since their cursor; ``POST /sync/push`` uploads the like/play events a client accumulated offline (idempotent — replays are no-ops). See :mod:`app.application.sync_service`. """ import datetime as dt from fastapi import APIRouter from app.api.deps import AlbumRepoDep, ArtistRepoDep, CurrentUser, SyncServiceDep from app.api.schemas.sync import ( LikeEventOut, PlayEventOut, PlaylistSyncOut, SyncChangesOut, SyncPushIn, SyncPushOut, ) from app.api.v1.tracks import _build_track_out from app.application.sync_service import LikeEvent, PlayEvent router = APIRouter(prefix="/sync", tags=["sync"]) @router.get("/changes") async def get_changes( service: SyncServiceDep, artist_repo: ArtistRepoDep, album_repo: AlbumRepoDep, user: CurrentUser, since: dt.datetime | None = None, ) -> SyncChangesOut: """Delta since ``since`` (omit for a full snapshot). Persist ``cursor`` from the response and pass it back as ``?since=`` next time.""" changes = await service.get_changes(user.id, since=since) artist_ids = list({t.artist_id for t in changes.tracks}) album_ids = list({t.album_id for t in changes.tracks if t.album_id is not None}) artists = {a.id: a for a in await artist_repo.get_many(artist_ids)} albums = {a.id: a for a in await album_repo.get_many(album_ids)} tracks_out = await _build_track_out(changes.tracks, artists, albums) return SyncChangesOut( cursor=changes.cursor, likes=[ LikeEventOut( id=lk.id, track_id=lk.track_id, value=lk.value, created_at=lk.created_at ) for lk in changes.likes ], plays=[ PlayEventOut( id=p.id, track_id=p.track_id, played_at=p.played_at, play_duration_seconds=p.play_duration_seconds, completed=p.completed, ) for p in changes.plays ], playlists=[ PlaylistSyncOut( id=d.playlist.id, name=d.playlist.name, description=d.playlist.description, version=d.playlist.version, updated_at=d.playlist.updated_at, track_ids=d.track_ids, ) for d in changes.playlists ], tracks=tracks_out, ) @router.post("/push") async def push_changes( body: SyncPushIn, service: SyncServiceDep, user: CurrentUser ) -> SyncPushOut: result = await service.push( user.id, likes=[ LikeEvent(id=e.id, track_id=e.track_id, value=e.value, created_at=e.created_at) for e in body.likes ], plays=[ PlayEvent( id=e.id, track_id=e.track_id, played_at=e.played_at, play_duration_seconds=e.play_duration_seconds, completed=e.completed, ) for e in body.plays ], ) return SyncPushOut( cursor=result.cursor, accepted_likes=result.accepted_likes, accepted_plays=result.accepted_plays, )