"""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.song import SongRepository from rehearsalhub.schemas.audio_version import AudioVersionCreate 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 remaining path segments # e.g. bands/my-band/songs/session1/rec.mp3 → folder=bands/my-band/songs/session1/, title=session1 # e.g. bands/my-band/rec.mp3 → folder=bands/my-band/, title=rec parent = str(Path(path).parent) nc_folder = parent.rstrip("/") + "/" title = Path(path).stem if len(parts) == 3 else parts[-2] 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"} 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, 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) # 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)}