Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions backend/app/routes/memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from fastapi import APIRouter
from datetime import datetime
from typing import List, Dict
from pydantic import BaseModel

from app.database.images import db_get_all_images

router = APIRouter(prefix="/memories", tags=["Memories"])


class Memory(BaseModel):
id: str
type: str # SPOTLIGHT | REVISIT | WEEKEND | YEAR
title: str
subtitle: str
image_ids: List[str]


def parse_date(img):
md = img.get("metadata") or {}
date_str = md.get("date_created")
if not date_str:
return None
try:
return datetime.fromisoformat(date_str)
except Exception:
return None


def format_date(d: datetime) -> str:
return d.strftime("%a, %d %b") # Mon, 22 Dec


def limit(ids: List[str], n=6):
return ids[:n]


@router.get("/")
async def get_memories():
images = db_get_all_images()
today = datetime.now()
Comment on lines +38 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sync database call in async endpoint blocks the event loop.

db_get_all_images() is a synchronous function (per the relevant code snippet showing it uses sqlite3). Calling it directly from an async def endpoint will block the event loop. Use run_in_executor or convert to async.

🔎 Proposed fix using run_in_executor
+ import asyncio
+ from concurrent.futures import ThreadPoolExecutor

+ executor = ThreadPoolExecutor(max_workers=2)

  @router.get("/")
  async def get_memories():
-     images = db_get_all_images()
+     loop = asyncio.get_event_loop()
+     images = await loop.run_in_executor(executor, db_get_all_images)
      today = datetime.now()

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/app/routes/memory.py around lines 38-41, the async endpoint calls the
synchronous db_get_all_images(), which will block the event loop; change the
call to run in a thread executor (or make the DB function async). Specifically,
import asyncio, get the running loop inside the endpoint and replace images =
db_get_all_images() with images = await loop.run_in_executor(None,
db_get_all_images) (or create an async wrapper that calls run_in_executor), then
continue using images as before; ensure any exceptions are propagated or caught
and that the function signature remains async.

memories: List[Memory] = []

# =====================================================
# 1️ SPOTLIGHT (TODAY)
# =====================================================
spotlight_ids = []
spotlight_date = None

for img in images:
d = parse_date(img)
if not d:
continue

if d.date() == today.date():
spotlight_ids.append(img["id"])
spotlight_date = d

if spotlight_ids:
memories.append(
Memory(
id="spotlight-today",
type="SPOTLIGHT",
title="Spotlight of the day",
subtitle=format_date(spotlight_date),
image_ids=limit(spotlight_ids),
)
)

# =====================================================
# 2️ REVISIT THE MOMENT
# =====================================================
revisit_ids = []
revisit_date = None

for img in images:
d = parse_date(img)
if not d:
continue

if d.day == today.day and d.month == today.month and d.year < today.year:
revisit_ids.append(img["id"])
revisit_date = d

if revisit_ids:
memories.append(
Memory(
id="revisit",
type="REVISIT",
title="Revisit the moment",
subtitle=format_date(revisit_date),
image_ids=limit(revisit_ids),
)
)

# =====================================================
# 3️ WEEKEND MEMORIES
# =====================================================
weekend_ids = []

for img in images:
d = parse_date(img)
if not d:
continue

if d.weekday() in (5, 6): # Sat, Sun
weekend_ids.append(img["id"])

if len(weekend_ids) >= 2:
memories.append(
Memory(
id="weekend",
type="WEEKEND",
title="Weekend memories",
subtitle="Relaxed moments",
image_ids=limit(weekend_ids),
)
)

# =====================================================
# 4️ YEAR MEMORIES (FIXED includes 2025)
# =====================================================
year_map: Dict[int, List[str]] = {}

for img in images:
d = parse_date(img)
if not d:
continue
year_map.setdefault(d.year, []).append(img["id"])

for year in sorted(year_map.keys(), reverse=True):
ids = year_map[year]
if len(ids) >= 2:
memories.append(
Memory(
id=f"year-{year}",
type="YEAR",
title=f"Memories from {year}",
subtitle=f"{len(ids)} photos",
image_ids=limit(ids),
)
)

return {"success": True, "data": memories}
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from app.routes.images import router as images_router
from app.routes.face_clusters import router as face_clusters_router
from app.routes.user_preferences import router as user_preferences_router
from app.routes.memory import router as memories_router
from fastapi.openapi.utils import get_openapi
from app.logging.setup_logging import (
configure_uvicorn_logging,
Expand Down Expand Up @@ -123,6 +124,7 @@ async def root():
return {"message": "PictoPy Server is up and running!"}


app.include_router(memories_router)
app.include_router(folders_router, prefix="/folders", tags=["Folders"])
app.include_router(albums_router, prefix="/albums", tags=["Albums"])
app.include_router(images_router, prefix="/images", tags=["Images"])
Expand Down
29 changes: 26 additions & 3 deletions docs/backend/backend_python/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@
}
}
},
"/memories/": {
"get": {
"tags": [
"Memories"
],
"summary": "Get Memories",
"operationId": "get_memories_memories__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/folders/add-folder": {
"post": {
"tags": [
Expand Down Expand Up @@ -1117,9 +1136,14 @@
"in": "query",
"required": false,
"schema": {
"$ref": "#/components/schemas/InputType",
"allOf": [
{
"$ref": "#/components/schemas/InputType"
}
],
"description": "Choose input type: 'path' or 'base64'",
"default": "path"
"default": "path",
"title": "Input Type"
},
"description": "Choose input type: 'path' or 'base64'"
}
Expand Down Expand Up @@ -2199,7 +2223,6 @@
"metadata": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,13 @@
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}


@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.animate-fadeIn {
animation: fadeIn 0.5s ease forwards;
}
10 changes: 10 additions & 0 deletions frontend/src/api/api-functions/memories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { apiClient } from '../axiosConfig';
import { memoriesEndpoints } from '../apiEndpoints';
import { APIResponse } from '@/types/API';

export const fetchMemories = async (): Promise<APIResponse> => {
const response = await apiClient.get<APIResponse>(
memoriesEndpoints.getAllMemories,
);
return response.data;
};
4 changes: 4 additions & 0 deletions frontend/src/api/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ export const userPreferencesEndpoints = {
export const healthEndpoints = {
healthCheck: '/health',
};

export const memoriesEndpoints = {
getAllMemories: '/memories',
};
Loading