diff --git a/api/src/rehearsalhub/routers/internal.py b/api/src/rehearsalhub/routers/internal.py index a3b4b72..dcdf624 100755 --- a/api/src/rehearsalhub/routers/internal.py +++ b/api/src/rehearsalhub/routers/internal.py @@ -12,7 +12,6 @@ from rehearsalhub.config import get_settings from rehearsalhub.db.engine import get_session from rehearsalhub.db.models import AudioVersion, BandMember, Member from rehearsalhub.queue.redis_queue import RedisJobQueue -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 @@ -91,36 +90,40 @@ async def nc_upload( if session_folder_path and session_folder_path.rstrip("/") == nc_folder.rstrip("/"): nc_folder = nc_folder + title + "/" - 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) + try: + 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) + except Exception as exc: + log.error("nc-upload: failed to resolve session for '%s': %s", path, exc, exc_info=True) + raise HTTPException(status_code=500, detail="Failed to resolve rehearsal session") from exc 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) + try: + 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) + except Exception as exc: + log.error("nc-upload: failed to find/create song for '%s': %s", path, exc, exc_info=True) + raise HTTPException(status_code=500, detail="Failed to resolve song") from exc # Use first member of the band as uploader (best-effort for watcher uploads) result = await session.execute( @@ -137,16 +140,24 @@ async def nc_upload( uploader = uploader_result.scalar_one_or_none() storage = NextcloudClient.for_member(uploader) if uploader else None - song_svc = SongService(session, storage=storage) - 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, - ) + try: + song_svc = SongService(session, storage=storage) + 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, + ) + except Exception as exc: + log.error( + "nc-upload: failed to register version for '%s' (song '%s'): %s", + path, song.title, exc, exc_info=True, + ) + raise HTTPException(status_code=500, detail="Failed to register version") from exc + 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)} diff --git a/api/src/rehearsalhub/services/nc_scan.py b/api/src/rehearsalhub/services/nc_scan.py index acb8aca..bbbc98a 100755 --- a/api/src/rehearsalhub/services/nc_scan.py +++ b/api/src/rehearsalhub/services/nc_scan.py @@ -9,7 +9,6 @@ from urllib.parse import unquote from sqlalchemy.ext.asyncio import AsyncSession -from rehearsalhub.repositories.audio_version import AudioVersionRepository from rehearsalhub.repositories.rehearsal_session import RehearsalSessionRepository from rehearsalhub.repositories.song import SongRepository from rehearsalhub.schemas.audio_version import AudioVersionCreate @@ -103,7 +102,6 @@ async def scan_band_folder( dav_prefix = f"/remote.php/dav/files/{nc._auth[0]}/" relative = _make_relative(dav_prefix) - version_repo = AudioVersionRepository(db_session) session_repo = RehearsalSessionRepository(db_session) song_repo = SongRepository(db_session) song_svc = SongService(db_session) @@ -133,68 +131,69 @@ async def scan_band_folder( meta = await nc.get_file_metadata(nc_file_path) etag = meta.etag except Exception as exc: - log.warning("Metadata error for '%s': %s", nc_file_path, exc) + log.error("Metadata fetch failed for '%s': %s", nc_file_path, exc, exc_info=True) + skipped += 1 yield {"type": "skipped", "path": nc_file_path, "reason": f"metadata error: {exc}"} continue - # Skip if this exact version is already indexed - if etag and await version_repo.get_by_etag(etag): - log.info("Already registered (etag match): %s", nc_file_path) - skipped += 1 - yield {"type": "skipped", "path": nc_file_path, "reason": "already registered"} - continue + try: + # Resolve or create a RehearsalSession from a YYMMDD folder segment + rehearsal_date = parse_rehearsal_date(nc_file_path) + rehearsal_session_id = None + if rehearsal_date: + session_folder = extract_session_folder(nc_file_path) or song_folder + rs = await session_repo.get_or_create(band_id, rehearsal_date, session_folder) + rehearsal_session_id = rs.id + yield { + "type": "session", + "session": { + "id": str(rs.id), + "date": rs.date.isoformat(), + "label": rs.label, + "nc_folder_path": rs.nc_folder_path, + }, + } - # Resolve or create a RehearsalSession from a YYMMDD folder segment - rehearsal_date = parse_rehearsal_date(nc_file_path) - rehearsal_session_id = None - if rehearsal_date: - session_folder = extract_session_folder(nc_file_path) or song_folder - rs = await session_repo.get_or_create(band_id, rehearsal_date, session_folder) - rehearsal_session_id = rs.id - yield { - "type": "session", - "session": { - "id": str(rs.id), - "date": rs.date.isoformat(), - "label": rs.label, - "nc_folder_path": rs.nc_folder_path, - }, - } + # Find or create the Song record + song = await song_repo.get_by_nc_folder_path(song_folder) + if song is None: + song = await song_repo.get_by_title_and_band(band_id, song_title) + is_new = song is None + if is_new: + log.info("Creating song '%s' folder='%s'", song_title, song_folder) + song = await song_repo.create( + band_id=band_id, + session_id=rehearsal_session_id, + title=song_title, + status="jam", + notes=None, + nc_folder_path=song_folder, + created_by=member_id, + ) + elif rehearsal_session_id and song.session_id is None: + song = await song_repo.update(song, session_id=rehearsal_session_id) - # Find or create the Song record - song = await song_repo.get_by_nc_folder_path(song_folder) - if song is None: - song = await song_repo.get_by_title_and_band(band_id, song_title) - is_new = song is None - if is_new: - log.info("Creating song '%s' folder='%s'", song_title, song_folder) - song = await song_repo.create( - band_id=band_id, - session_id=rehearsal_session_id, - title=song_title, - status="jam", - notes=None, - nc_folder_path=song_folder, - created_by=member_id, + # Register the audio version + version = await song_svc.register_version( + song.id, + AudioVersionCreate( + nc_file_path=nc_file_path, + nc_file_etag=etag, + format=Path(nc_file_path).suffix.lstrip(".").lower(), + file_size_bytes=meta.size, + ), + member_id, ) - elif rehearsal_session_id and song.session_id is None: - song = await song_repo.update(song, session_id=rehearsal_session_id) + log.info("Imported '%s' as version %s for song '%s'", nc_file_path, version.id, song.title) - # Register the audio version - await song_svc.register_version( - song.id, - AudioVersionCreate( - nc_file_path=nc_file_path, - nc_file_etag=etag, - format=Path(nc_file_path).suffix.lstrip(".").lower(), - file_size_bytes=meta.size, - ), - member_id, - ) + imported += 1 + read = SongRead.model_validate(song).model_copy(update={"version_count": 1, "session_id": rehearsal_session_id}) + yield {"type": "song", "song": read.model_dump(mode="json"), "is_new": is_new} - imported += 1 - read = SongRead.model_validate(song).model_copy(update={"version_count": 1, "session_id": rehearsal_session_id}) - yield {"type": "song", "song": read.model_dump(mode="json"), "is_new": is_new} + except Exception as exc: + log.error("Failed to import '%s': %s", nc_file_path, exc, exc_info=True) + skipped += 1 + yield {"type": "skipped", "path": nc_file_path, "reason": f"import error: {exc}"} yield { "type": "done", diff --git a/api/src/rehearsalhub/services/song.py b/api/src/rehearsalhub/services/song.py index 00861b2..c3c9bfc 100755 --- a/api/src/rehearsalhub/services/song.py +++ b/api/src/rehearsalhub/services/song.py @@ -1,9 +1,12 @@ from __future__ import annotations +import logging import uuid from sqlalchemy.ext.asyncio import AsyncSession +log = logging.getLogger(__name__) + from rehearsalhub.db.models import AudioVersion, Song from rehearsalhub.queue.redis_queue import RedisJobQueue from rehearsalhub.repositories.audio_version import AudioVersionRepository @@ -67,11 +70,6 @@ class SongService: data: AudioVersionCreate, uploader_id: uuid.UUID, ) -> AudioVersion: - if data.nc_file_etag: - existing = await self._version_repo.get_by_etag(data.nc_file_etag) - if existing: - return existing - version_number = await self._repo.next_version_number(song_id) version = await self._version_repo.create( song_id=song_id, @@ -85,8 +83,15 @@ class SongService: uploaded_by=uploader_id, ) - await self._queue.enqueue( - "transcode", - {"version_id": str(version.id), "nc_file_path": data.nc_file_path}, - ) + try: + await self._queue.enqueue( + "transcode", + {"version_id": str(version.id), "nc_file_path": data.nc_file_path}, + ) + except Exception as exc: + log.error( + "Failed to enqueue transcode job for version %s ('%s'): %s", + version.id, data.nc_file_path, exc, exc_info=True, + ) + return version