Files
Senko-san 7c920f38f6 feat: implement 1B domain entities/repos + 1H library API routes
1B — domain layer:
- New entities: Album, Playlist, Like, PlayHistoryEntry
- Track entity extended with album_id, genre, year fields
- New protocols: AlbumRepository, PlaylistRepository, LikeRepository, HistoryRepository
- ArtistRepository / TrackRepository protocols extended (list, count, update, get_many, etc.)
- New repos: SqlAlchemyAlbum/Playlist/Like/HistoryRepository
- Artist and track repos updated to match extended protocols

1H — library API:
- Pagination: PagedResponse[T] generic, offset-based, limit default 50 max 200
- Schemas: TrackOut, AlbumOut, ArtistOut, PlaylistOut/Create/Update,
  LikeEvent/State, HistoryIn/Out, LibrarySearchResponse
- GET/PATCH/DELETE /tracks with filters, sort, pagination
- GET /albums, /albums/{id}, /albums/{id}/tracks
- GET /artists, /artists/{id}, /artists/{id}/albums, /artists/{id}/tracks
- GET /search/library (ILIKE across tracks/albums/artists)
- Full /playlists CRUD + track add/remove (append-only version bump)
- POST /likes (append-only event log), GET /likes, GET /likes/state
- POST /history (scrobble), GET /history
- deps.py: TrackRepoDep, ArtistRepoDep, AlbumRepoDep, PlaylistRepoDep,
  LikeRepoDep, HistoryRepoDep

ruff   mypy   pytest 45/45 

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 16:43:51 +03:00

64 lines
2.1 KiB
Python

"""Like endpoints. Likes are an append-only event-log — never updated in place."""
import uuid
from fastapi import APIRouter, Query
from app.api.deps import AlbumRepoDep, ArtistRepoDep, CurrentUser, LikeRepoDep
from app.api.schemas.like import LikeEvent, LikeState
from app.api.schemas.pagination import PagedResponse
from app.api.schemas.track import TrackOut
from app.api.v1.tracks import _build_track_out
router = APIRouter(prefix="/likes", tags=["likes"])
@router.post("", status_code=201)
async def add_like(
body: LikeEvent,
like_repo: LikeRepoDep,
user: CurrentUser,
) -> LikeState:
like = await like_repo.add(user_id=user.id, track_id=body.track_id, value=body.value)
return LikeState(track_id=like.track_id, value=like.value, updated_at=like.created_at)
@router.get("")
async def get_likes(
like_repo: LikeRepoDep,
artist_repo: ArtistRepoDep,
album_repo: AlbumRepoDep,
user: CurrentUser,
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
) -> PagedResponse[TrackOut]:
tracks = await like_repo.list_liked_tracks(user_id=user.id, limit=limit, offset=offset)
total = await like_repo.count_liked_tracks(user_id=user.id)
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})
artists_map = {a.id: a for a in await artist_repo.get_many(artist_ids)}
albums_map = {a.id: a for a in await album_repo.get_many(album_ids)}
items = await _build_track_out(tracks, artists_map, albums_map)
return PagedResponse(items=items, total=total, limit=limit, offset=offset)
@router.get("/state")
async def get_likes_state(
like_repo: LikeRepoDep,
user: CurrentUser,
track_ids: str = Query(default=""),
) -> list[LikeState]:
ids: list[uuid.UUID] = []
if track_ids:
try:
ids = [uuid.UUID(tid.strip()) for tid in track_ids.split(",") if tid.strip()]
except ValueError:
return []
likes = await like_repo.get_latest_state(user_id=user.id, track_ids=ids)
return [
LikeState(track_id=lk.track_id, value=lk.value, updated_at=lk.created_at) for lk in likes
]