fix: scan visibility, NC folder validation, watcher logging
- nc-scan: detailed INFO logging of every path found, subfolder contents and skip reasons; 502 now includes the exact folder and error so user sees a real message instead of a blank result - band creation: if nc_base_path is explicitly given, verify the folder exists in Nextcloud before saving — returns 422 with a clear message to the user; auto-generated paths still do MKCOL - songs search: add ?unattributed=true to return songs with no session_id (files not in a YYMMDD folder) - BandPage: show "Unattributed Recordings" section below sessions so scanned files without a dated folder always appear - watcher event_loop: promote all per-activity log lines from DEBUG to INFO so they're visible in default Docker Compose log output; log normalized path and skip reason for every activity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ class SongRepository(BaseRepository[Song]):
|
||||
bpm_min: float | None = None,
|
||||
bpm_max: float | None = None,
|
||||
session_id: uuid.UUID | None = None,
|
||||
unattributed: bool = False,
|
||||
) -> list[Song]:
|
||||
from sqlalchemy import cast, func
|
||||
from sqlalchemy.dialects.postgresql import ARRAY
|
||||
@@ -75,6 +76,8 @@ class SongRepository(BaseRepository[Song]):
|
||||
stmt = stmt.where(Song.global_bpm <= bpm_max)
|
||||
if session_id is not None:
|
||||
stmt = stmt.where(Song.session_id == session_id)
|
||||
if unattributed:
|
||||
stmt = stmt.where(Song.session_id.is_(None))
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@@ -34,6 +34,8 @@ async def create_band(
|
||||
band = await svc.create_band(data, current_member.id, creator=current_member)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
|
||||
except LookupError as e:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
|
||||
return BandRead.model_validate(band)
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ async def search_songs(
|
||||
bpm_min: float | None = Query(None, ge=0),
|
||||
bpm_max: float | None = Query(None, ge=0),
|
||||
session_id: uuid.UUID | None = Query(None),
|
||||
unattributed: bool = Query(False, description="Only songs with no rehearsal session"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_member: Member = Depends(get_current_member),
|
||||
):
|
||||
@@ -78,6 +79,7 @@ async def search_songs(
|
||||
bpm_min=bpm_min,
|
||||
bpm_max=bpm_max,
|
||||
session_id=session_id,
|
||||
unattributed=unattributed,
|
||||
)
|
||||
return [
|
||||
SongRead.model_validate(s, update={"version_count": len(s.versions)})
|
||||
@@ -173,14 +175,20 @@ async def scan_nextcloud(
|
||||
skipped_count = 0
|
||||
band_folder = band.nc_folder_path or f"bands/{band.slug}/"
|
||||
|
||||
log.info("Starting NC scan for band '%s' in folder '%s'", band.slug, band_folder)
|
||||
log.info("NC scan START — band='%s' folder='%s' nc_user='%s'", band.slug, band_folder, nc._auth[0])
|
||||
|
||||
try:
|
||||
items = await nc.list_folder(band_folder)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Nextcloud unreachable: {exc}")
|
||||
log.error("NC scan FAILED — could not list '%s': %s", band_folder, exc)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Cannot read Nextcloud folder '{band_folder}': {exc}",
|
||||
)
|
||||
|
||||
log.info("Found %d top-level entries in '%s'", len(items), band_folder)
|
||||
log.info("NC scan — found %d top-level entries in '%s'", len(items), band_folder)
|
||||
for item in items:
|
||||
log.info(" entry href=%s → rel=%s", item.path, relative(item.path))
|
||||
|
||||
# Collect (nc_file_path, nc_folder, song_title, rehearsal_label) tuples.
|
||||
# nc_folder is the directory that groups versions of the same song.
|
||||
@@ -195,27 +203,39 @@ async def scan_nextcloud(
|
||||
try:
|
||||
sub_items = await nc.list_folder(rel)
|
||||
except Exception as exc:
|
||||
log.warning("Could not list subfolder '%s': %s", rel, exc)
|
||||
log.warning("NC scan — could not list subfolder '%s': %s", rel, exc)
|
||||
continue
|
||||
|
||||
all_sub = [relative(s.path) for s in sub_items]
|
||||
audio_files = [s for s in sub_items if Path(relative(s.path)).suffix.lower() in AUDIO_EXTENSIONS]
|
||||
log.info("Subfolder '%s': %d audio files found", dir_name, len(audio_files))
|
||||
log.info(
|
||||
"NC scan — subfolder '%s': %d entries total, %d audio files",
|
||||
dir_name, len(all_sub), len(audio_files),
|
||||
)
|
||||
for s in sub_items:
|
||||
sr = relative(s.path)
|
||||
ext = Path(sr).suffix.lower()
|
||||
if ext and ext not in AUDIO_EXTENSIONS:
|
||||
log.info(" skip (not audio ext=%s): %s", ext, sr)
|
||||
|
||||
for sub in audio_files:
|
||||
sub_rel = relative(sub.path)
|
||||
song_title = Path(sub_rel).stem
|
||||
# Each file in a rehearsal folder is its own song,
|
||||
# grouped under its own sub-subfolder path for version tracking.
|
||||
song_folder = str(Path(sub_rel).parent) + "/"
|
||||
rehearsal_label = dir_name # e.g. "231015" or "2023-10-15"
|
||||
rehearsal_label = dir_name
|
||||
log.info(" queue for import: %s → title='%s' folder='%s'", sub_rel, song_title, song_folder)
|
||||
to_import.append((sub_rel, song_folder, song_title, rehearsal_label))
|
||||
else:
|
||||
if Path(rel).suffix.lower() in AUDIO_EXTENSIONS:
|
||||
ext = Path(rel).suffix.lower()
|
||||
if ext in AUDIO_EXTENSIONS:
|
||||
folder = str(Path(rel).parent) + "/"
|
||||
title = Path(rel).stem
|
||||
log.info(" queue for import (root-level): %s → title='%s'", rel, title)
|
||||
to_import.append((rel, folder, title, None))
|
||||
elif ext:
|
||||
log.info(" skip root-level (not audio ext=%s): %s", ext, rel)
|
||||
|
||||
log.info("NC scan: %d audio files to evaluate for import", len(to_import))
|
||||
log.info("NC scan — %d audio files queued for import", len(to_import))
|
||||
|
||||
song_repo = SongRepository(session)
|
||||
from rehearsalhub.schemas.audio_version import AudioVersionCreate # noqa: PLC0415
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -9,22 +10,42 @@ from rehearsalhub.repositories.band import BandRepository
|
||||
from rehearsalhub.schemas.band import BandCreate, BandReadWithMembers
|
||||
from rehearsalhub.storage.nextcloud import NextcloudClient
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BandService:
|
||||
def __init__(self, session: AsyncSession, storage: NextcloudClient | None = None) -> None:
|
||||
self._repo = BandRepository(session)
|
||||
self._storage = storage or NextcloudClient()
|
||||
|
||||
async def create_band(self, data: BandCreate, creator_id: uuid.UUID, creator: object | None = None) -> Band:
|
||||
async def create_band(
|
||||
self,
|
||||
data: BandCreate,
|
||||
creator_id: uuid.UUID,
|
||||
creator: object | None = None,
|
||||
) -> Band:
|
||||
if await self._repo.get_by_slug(data.slug):
|
||||
raise ValueError(f"Slug already taken: {data.slug}")
|
||||
|
||||
nc_folder = (data.nc_base_path or f"bands/{data.slug}/").strip("/") + "/"
|
||||
storage = NextcloudClient.for_member(creator) if creator else self._storage
|
||||
try:
|
||||
await storage.create_folder(nc_folder)
|
||||
except Exception:
|
||||
pass # NC might not be reachable during tests; folder creation is best-effort
|
||||
|
||||
if data.nc_base_path:
|
||||
# User explicitly specified a folder — verify it actually exists in NC.
|
||||
log.info("Checking NC folder existence: %s", nc_folder)
|
||||
try:
|
||||
await storage.get_file_metadata(nc_folder.rstrip("/"))
|
||||
except Exception as exc:
|
||||
log.warning("NC folder '%s' not accessible: %s", nc_folder, exc)
|
||||
raise LookupError(f"Nextcloud folder '{nc_folder}' not found or not accessible")
|
||||
else:
|
||||
# Auto-generated path — create it (idempotent MKCOL).
|
||||
log.info("Creating NC folder: %s", nc_folder)
|
||||
try:
|
||||
await storage.create_folder(nc_folder)
|
||||
except Exception as exc:
|
||||
# Not fatal — NC may be temporarily unreachable during dev/test.
|
||||
log.warning("Could not create NC folder '%s': %s", nc_folder, exc)
|
||||
|
||||
band = await self._repo.create(
|
||||
name=data.name,
|
||||
@@ -33,6 +54,7 @@ class BandService:
|
||||
nc_folder_path=nc_folder,
|
||||
)
|
||||
await self._repo.add_member(band.id, creator_id, role="admin")
|
||||
log.info("Created band '%s' (slug=%s, nc_folder=%s)", data.name, data.slug, nc_folder)
|
||||
return band
|
||||
|
||||
async def get_band_with_members(self, band_id: uuid.UUID) -> Band | None:
|
||||
|
||||
Reference in New Issue
Block a user