fix: robust NC activity filter, title extraction, scan result detail

Watcher:
- Accept both NC 22+ (type="file_created") and older NC (subject="created_self")
  so the upload filter works across all Nextcloud versions
- Add .opus to audio_extensions
- Fix tests: set nc.username on mocks, use realistic activity dicts with type field
- Add tests for old NC style, non-band path filter, normalize_nc_path, cursor advance

API:
- Fix internal.py title extraction: always use filename stem (was using
  parts[-2] for >3-part paths, which gave folder name instead of song title)
- nc-scan now returns NcScanResult with folder, files_found, imported, skipped counts
  instead of bare song list — gives the UI actionable feedback

Web:
- Show rich scan result message: folder scanned, count imported, count already registered

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Steffen Schuhmann
2026-03-29 12:26:58 +02:00
parent fbac62a0ea
commit 47bc802775
6 changed files with 179 additions and 46 deletions

View File

@@ -26,6 +26,14 @@ interface BandInvite {
expires_at: string;
}
interface NcScanResult {
folder: string;
files_found: number;
imported: number;
skipped: number;
songs: SongSummary[];
}
export function BandPage() {
const { bandId } = useParams<{ bandId: string }>();
const qc = useQueryClient();
@@ -67,15 +75,17 @@ export function BandPage() {
});
const scanMutation = useMutation({
mutationFn: () => api.post<SongSummary[]>(`/bands/${bandId}/nc-scan`, {}),
onSuccess: (imported) => {
mutationFn: () => api.post<NcScanResult>(`/bands/${bandId}/nc-scan`, {}),
onSuccess: (result) => {
qc.invalidateQueries({ queryKey: ["songs", bandId] });
setScanMsg(
imported.length > 0
? `Imported ${imported.length} new song${imported.length !== 1 ? "s" : ""} from Nextcloud.`
: "No new audio files found in Nextcloud."
);
setTimeout(() => setScanMsg(null), 4000);
if (result.imported > 0) {
setScanMsg(`Imported ${result.imported} new song${result.imported !== 1 ? "s" : ""} from ${result.folder} (${result.skipped} already registered).`);
} else if (result.files_found === 0) {
setScanMsg(`No audio files found in ${result.folder}.`);
} else {
setScanMsg(`All ${result.files_found} file${result.files_found !== 1 ? "s" : ""} in ${result.folder} already registered.`);
}
setTimeout(() => setScanMsg(null), 6000);
},
onError: (err) => setScanMsg(err instanceof Error ? err.message : "Scan failed"),
});