Files
mcma-backend/app/infrastructure/db/repositories/like_repository.py
T
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

151 lines
4.7 KiB
Python

"""Like repository — adapter over ``AsyncSession``.
Likes are an append-only event log. Current state = latest event per (user, track).
"""
import uuid
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.entities.like import Like
from app.domain.entities.track import Track
from app.infrastructure.db.models.like import LikeModel
from app.infrastructure.db.models.track import TrackModel
def _to_entity(row: LikeModel) -> Like:
return Like(
id=row.id,
user_id=row.user_id,
track_id=row.track_id,
value=row.value,
created_at=row.created_at,
)
def _track_to_entity(row: TrackModel) -> Track:
return Track(
id=row.id,
title=row.title,
artist_id=row.artist_id,
album_id=row.album_id,
file_path=row.file_path,
file_format=row.file_format,
file_size=row.file_size,
source=row.source,
source_id=row.source_id,
duration_seconds=row.duration_seconds,
genre=row.genre,
year=row.year,
metadata_status=row.metadata_status,
created_at=row.created_at,
updated_at=row.updated_at,
)
class SqlAlchemyLikeRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def add(self, *, user_id: uuid.UUID, track_id: uuid.UUID, value: str) -> Like:
row = LikeModel(user_id=user_id, track_id=track_id, value=value)
self._session.add(row)
await self._session.flush()
await self._session.refresh(row)
return _to_entity(row)
async def get_latest_state(
self, *, user_id: uuid.UUID, track_ids: list[uuid.UUID]
) -> list[Like]:
if not track_ids:
return []
# Subquery: max(created_at) per track for this user
max_sq = (
select(
LikeModel.track_id,
func.max(LikeModel.created_at).label("latest"),
)
.where(LikeModel.user_id == user_id, LikeModel.track_id.in_(track_ids))
.group_by(LikeModel.track_id)
.subquery()
)
rows = (
(
await self._session.execute(
select(LikeModel)
.join(
max_sq,
(LikeModel.track_id == max_sq.c.track_id)
& (LikeModel.created_at == max_sq.c.latest),
)
.where(LikeModel.user_id == user_id)
)
)
.scalars()
.all()
)
return [_to_entity(r) for r in rows]
async def list_liked_tracks(
self, *, user_id: uuid.UUID, limit: int, offset: int
) -> list[Track]:
# Tracks where the latest like event has value='like', ordered by like time desc
max_sq = (
select(
LikeModel.track_id,
func.max(LikeModel.created_at).label("latest"),
)
.where(LikeModel.user_id == user_id)
.group_by(LikeModel.track_id)
.subquery()
)
liked_sq = (
select(LikeModel.track_id, LikeModel.created_at)
.join(
max_sq,
(LikeModel.track_id == max_sq.c.track_id)
& (LikeModel.created_at == max_sq.c.latest),
)
.where(LikeModel.user_id == user_id, LikeModel.value == "like")
.subquery()
)
rows = (
(
await self._session.execute(
select(TrackModel)
.join(liked_sq, TrackModel.id == liked_sq.c.track_id)
.order_by(liked_sq.c.created_at.desc())
.limit(limit)
.offset(offset)
)
)
.scalars()
.all()
)
return [_track_to_entity(r) for r in rows]
async def count_liked_tracks(self, *, user_id: uuid.UUID) -> int:
max_sq = (
select(
LikeModel.track_id,
func.max(LikeModel.created_at).label("latest"),
)
.where(LikeModel.user_id == user_id)
.group_by(LikeModel.track_id)
.subquery()
)
liked_sq = (
select(LikeModel.track_id)
.join(
max_sq,
(LikeModel.track_id == max_sq.c.track_id)
& (LikeModel.created_at == max_sq.c.latest),
)
.where(LikeModel.user_id == user_id, LikeModel.value == "like")
.subquery()
)
return (
await self._session.execute(select(func.count()).select_from(liked_sq))
).scalar_one()