feat: "On this day" memories — photos from previous years

Add a Memories view that surfaces photos taken on the current date in
previous years (like Google Photos / Immich). Only uses EXIF-sourced
dates to avoid false matches from filesystem timestamps.

- Backend: GET /api/v1/photos/memories returns groups by year, up to
  12 photos each, filtered to non-discarded/non-hidden EXIF dates
- Frontend: MemoriesView with year-grouped thumbnail grid
- Sidebar: new "Memories" nav item with clock icon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 22:18:28 +02:00
parent bbb8e4850c
commit fc8dd370c2
5 changed files with 174 additions and 0 deletions

View File

@@ -352,6 +352,74 @@ async def list_photos_with_gps(
]
@router.get("/memories")
async def get_memories(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""'On this day' — photos taken on this date in previous years.
Returns groups keyed by year, each with up to 12 photos. Only
considers non-discarded, non-hidden photos with an EXIF-sourced
taken_at (no filesystem-guessed dates to avoid false matches).
"""
from sqlalchemy import extract
today = datetime.now().date()
result = await db.execute(
select(
Photo.id,
Photo.filename,
Photo.taken_at,
Photo.thumb_small,
Photo.thumb_medium,
Photo.media_type,
Photo.width,
Photo.height,
Photo.rating,
)
.where(
Photo.user_id == current_user.id,
Photo.is_discarded.is_(False),
Photo.is_hidden.is_(False),
Photo.taken_at.is_not(None),
Photo.taken_at_source == "exif",
extract("month", Photo.taken_at) == today.month,
extract("day", Photo.taken_at) == today.day,
extract("year", Photo.taken_at) < today.year,
)
.order_by(Photo.taken_at.desc())
)
rows = result.all()
# Group by year
years: dict[int, list] = {}
for row in rows:
year = row.taken_at.year
group = years.setdefault(year, [])
if len(group) >= 12:
continue
group.append({
"id": row.id,
"filename": row.filename,
"taken_at": row.taken_at.isoformat(),
"thumb_small": row.thumb_small,
"thumb_medium": row.thumb_medium,
"media_type": row.media_type,
"width": row.width,
"height": row.height,
"rating": row.rating,
})
memories = [
{"year": year, "years_ago": today.year - year, "photos": photos}
for year, photos in sorted(years.items())
]
return {"date": today.isoformat(), "memories": memories}
@router.get("/{photo_id}")
async def get_photo(
photo_id: str,