From 7dcfa8f30d24493f52bfe243be11e27fab98a368 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 7 Apr 2026 23:39:33 +0200 Subject: [PATCH] fix: photos folder_id filter accepts source root ids too GET /folders returns SourceRoot rows (the top-level scan paths shown in the LeftSidebar tree), but photos.folder_id points to a Folder row (a directory under a source root), and the photos list endpoint was matching Photo.folder_id == folder_id literally. Result: clicking "MulitaTest" in the sidebar sent the source root id, which never matched any photo, so the timeline went empty even though the photo_count badge showed 5. Fix: when the folder_id param matches a SourceRoot, expand it to every child Folder.id under that root and use IN. Falls back to the literal match for actual folder ids. If a source root has no child folder rows yet, returns no photos (rather than the whole library) so a half-scanned root doesn't accidentally show everything. The longer-term cleanup is to deduplicate the source_root / folder rows the scanner is creating on each rescan, but this makes the navigation work today. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/routers/photos.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/app/routers/photos.py b/backend/app/routers/photos.py index 8e4b464..2784df1 100644 --- a/backend/app/routers/photos.py +++ b/backend/app/routers/photos.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) from app.database import get_db from app.models import Photo, Folder, Tag, PhotoTag +from app.models.folders import SourceRoot from app.models.heaps import heap_photos from app.schemas.photos import PhotoResponse, PhotoUpdate, PhotoListResponse, BulkAction from app.config import settings @@ -67,9 +68,27 @@ async def list_photos( if date_to: filters.append(Photo.taken_at <= date_to) - # Folder filter + # Folder filter — the sidebar exposes "source roots" (top-level scan + # paths) under the same UI affordance as folders, so the same param has + # to accept either a folder id or a source root id. If the value matches + # a source root, expand to every folder under that root and use IN. if folder_id: - filters.append(Photo.folder_id == folder_id) + sr_check = await db.execute( + select(SourceRoot.id).where(SourceRoot.id == folder_id) + ) + if sr_check.scalar_one_or_none() is not None: + child_folders = await db.execute( + select(Folder.id).where(Folder.source_root_id == folder_id) + ) + child_ids = [row[0] for row in child_folders.all()] + if child_ids: + filters.append(Photo.folder_id.in_(child_ids)) + else: + # Source root with no folder rows yet — match nothing rather + # than returning the entire library. + filters.append(Photo.id == '__no_match__') + else: + filters.append(Photo.folder_id == folder_id) # Media type filter if media_type: