diff --git a/api/src/rehearsalhub/repositories/rehearsal_session.py b/api/src/rehearsalhub/repositories/rehearsal_session.py new file mode 100644 index 0000000..b0e0208 --- /dev/null +++ b/api/src/rehearsalhub/repositories/rehearsal_session.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import uuid +from datetime import date, datetime + +from sqlalchemy import func, select +from sqlalchemy.orm import selectinload + +from rehearsalhub.db.models import RehearsalSession, Song +from rehearsalhub.repositories.base import BaseRepository + + +class RehearsalSessionRepository(BaseRepository[RehearsalSession]): + model = RehearsalSession + + async def get_by_nc_folder(self, band_id: uuid.UUID, nc_folder_path: str) -> RehearsalSession | None: + stmt = select(RehearsalSession).where( + RehearsalSession.band_id == band_id, + RehearsalSession.nc_folder_path == nc_folder_path, + ) + result = await self.session.execute(stmt) + return result.scalar_one_or_none() + + async def get_by_band_and_date(self, band_id: uuid.UUID, session_date: date) -> RehearsalSession | None: + # Match on date portion only (stored as DateTime(timezone=False)) + day_start = datetime(session_date.year, session_date.month, session_date.day) + stmt = select(RehearsalSession).where( + RehearsalSession.band_id == band_id, + RehearsalSession.date == day_start, + ) + result = await self.session.execute(stmt) + return result.scalar_one_or_none() + + async def get_or_create( + self, + band_id: uuid.UUID, + session_date: date, + nc_folder_path: str, + ) -> RehearsalSession: + existing = await self.get_by_band_and_date(band_id, session_date) + if existing is not None: + return existing + return await self.create( + band_id=band_id, + date=datetime(session_date.year, session_date.month, session_date.day), + nc_folder_path=nc_folder_path, + ) + + async def list_for_band(self, band_id: uuid.UUID) -> list[tuple[RehearsalSession, int]]: + """Return (session, recording_count) tuples, newest date first.""" + count_col = func.count(Song.id).label("recording_count") + stmt = ( + select(RehearsalSession, count_col) + .outerjoin(Song, Song.session_id == RehearsalSession.id) + .where(RehearsalSession.band_id == band_id) + .group_by(RehearsalSession.id) + .order_by(RehearsalSession.date.desc()) + ) + result = await self.session.execute(stmt) + return [(row[0], row[1]) for row in result.all()] + + async def get_with_songs(self, session_id: uuid.UUID) -> RehearsalSession | None: + stmt = ( + select(RehearsalSession) + .options(selectinload(RehearsalSession.songs).selectinload(Song.versions)) + .where(RehearsalSession.id == session_id) + ) + result = await self.session.execute(stmt) + return result.scalar_one_or_none() diff --git a/api/src/rehearsalhub/schemas/rehearsal_session.py b/api/src/rehearsalhub/schemas/rehearsal_session.py new file mode 100644 index 0000000..2cedf82 --- /dev/null +++ b/api/src/rehearsalhub/schemas/rehearsal_session.py @@ -0,0 +1,27 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel, ConfigDict + +from rehearsalhub.schemas.song import SongRead + + +class RehearsalSessionRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: uuid.UUID + band_id: uuid.UUID + date: datetime + nc_folder_path: str | None = None + label: str | None = None + notes: str | None = None + created_at: datetime + recording_count: int = 0 + + +class RehearsalSessionDetail(RehearsalSessionRead): + songs: list[SongRead] = [] + + +class RehearsalSessionUpdate(BaseModel): + label: str | None = None + notes: str | None = None diff --git a/api/src/rehearsalhub/schemas/song.py b/api/src/rehearsalhub/schemas/song.py index 347044e..95d6730 100644 --- a/api/src/rehearsalhub/schemas/song.py +++ b/api/src/rehearsalhub/schemas/song.py @@ -15,6 +15,7 @@ class SongUpdate(BaseModel): title: str | None = None status: Literal["jam", "wip", "arranged", "recorded", "released"] | None = None notes: str | None = None + tags: list[str] | None = None global_key: str | None = None global_bpm: float | None = None @@ -23,8 +24,10 @@ class SongRead(BaseModel): model_config = ConfigDict(from_attributes=True) id: uuid.UUID band_id: uuid.UUID + session_id: uuid.UUID | None = None title: str status: str + tags: list[str] = [] global_key: str | None = None global_bpm: float | None = None notes: str | None = None