"""Internal endpoints — called by trusted services (watcher) on the Docker network.""" import logging from pathlib import Path from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from rehearsalhub.db.engine import get_session from rehearsalhub.repositories.audio_version import AudioVersionRepository from rehearsalhub.repositories.band import BandRepository from rehearsalhub.repositories.rehearsal_session import RehearsalSessionRepository from rehearsalhub.repositories.song import SongRepository from rehearsalhub.schemas.audio_version import AudioVersionCreate from rehearsalhub.services.session import parse_rehearsal_date from rehearsalhub.services.song import SongService log = logging.getLogger(__name__) router = APIRouter(prefix="/internal", tags=["internal"]) AUDIO_EXTENSIONS = {".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".opus"} class NcUploadEvent(BaseModel): nc_file_path: str nc_file_etag: str | None = None @router.post("/nc-upload", status_code=200) async def nc_upload( event: NcUploadEvent, session: AsyncSession = Depends(get_session), ): """ Called by nc-watcher when a new audio file is detected in Nextcloud. Parses the path to find/create the band+song and registers a version. Expected path format: bands/{slug}/[songs/]{folder}/filename.ext """ path = event.nc_file_path.lstrip("/") if Path(path).suffix.lower() not in AUDIO_EXTENSIONS: return {"status": "skipped", "reason": "not an audio file"} parts = path.split("/") if len(parts) < 3 or parts[0] != "bands": return {"status": "skipped", "reason": "path not under bands/"} band_slug = parts[1] band_repo = BandRepository(session) band = await band_repo.get_by_slug(band_slug) if band is None: log.warning("nc-upload: band slug '%s' not found in DB", band_slug) return {"status": "skipped", "reason": "band not found"} # Determine song title and folder from path. # The title is always the filename stem (e.g. "take1" from "take1.wav"). # The nc_folder groups all versions of the same recording (the parent directory). # # Examples: # bands/my-band/take1.wav → folder=bands/my-band/, title=take1 # bands/my-band/231015/take1.wav → folder=bands/my-band/231015/, title=take1 # bands/my-band/songs/groove/take1.wav → folder=bands/my-band/songs/groove/, title=take1 parent = str(Path(path).parent) nc_folder = parent.rstrip("/") + "/" title = Path(path).stem version_repo = AudioVersionRepository(session) if event.nc_file_etag and await version_repo.get_by_etag(event.nc_file_etag): return {"status": "skipped", "reason": "version already registered"} # Resolve or create rehearsal session from YYMMDD folder segment session_repo = RehearsalSessionRepository(session) rehearsal_date = parse_rehearsal_date(path) rehearsal_session_id = None if rehearsal_date: rehearsal_session = await session_repo.get_or_create(band.id, rehearsal_date, nc_folder) rehearsal_session_id = rehearsal_session.id log.debug("nc-upload: linked to session %s (%s)", rehearsal_session_id, rehearsal_date) song_repo = SongRepository(session) song = await song_repo.get_by_nc_folder_path(nc_folder) if song is None: song = await song_repo.get_by_title_and_band(band.id, title) if song is None: song = await song_repo.create( band_id=band.id, session_id=rehearsal_session_id, title=title, status="jam", notes=None, nc_folder_path=nc_folder, created_by=None, ) log.info("nc-upload: created song '%s' for band '%s'", title, band_slug) elif rehearsal_session_id and song.session_id is None: song = await song_repo.update(song, session_id=rehearsal_session_id) # Use first member of the band as uploader (best-effort for watcher uploads) from sqlalchemy import select from rehearsalhub.db.models import BandMember result = await session.execute( select(BandMember.member_id).where(BandMember.band_id == band.id).limit(1) ) uploader_id = result.scalar_one_or_none() song_svc = SongService(session) version = await song_svc.register_version( song.id, AudioVersionCreate( nc_file_path=path, nc_file_etag=event.nc_file_etag, format=Path(path).suffix.lstrip(".").lower(), ), uploader_id, ) log.info("nc-upload: registered version %s for song '%s'", version.id, song.title) return {"status": "ok", "version_id": str(version.id), "song_id": str(song.id)}