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:
Steffen Schuhmann
2026-03-29 14:11:07 +02:00
parent 25502458d0
commit dc6dd9dcfd
6 changed files with 128 additions and 34 deletions

View File

@@ -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