diff --git a/app/routers/library.py b/app/routers/library.py
index 99630a6..8b1cfac 100644
--- a/app/routers/library.py
+++ b/app/routers/library.py
@@ -33,6 +33,28 @@ async def library_stats(request: Request):
await db.close()
+@router.post("/watch-check")
+async def watch_check(request: Request):
+ """Given a list of jellyfin_ids, return which ones the current user has watched."""
+ user = await get_current_user(request)
+ body = await request.json()
+ ids = body.get("jellyfin_ids", [])
+ if not ids:
+ return {"watched": []}
+
+ db = await get_db()
+ try:
+ placeholders = ",".join("?" for _ in ids)
+ cursor = await db.execute(
+ f"SELECT jellyfin_id FROM watch_state WHERE user_id = ? AND is_played = 1 AND jellyfin_id IN ({placeholders})",
+ [user["id"]] + ids,
+ )
+ rows = await cursor.fetchall()
+ return {"watched": [row["jellyfin_id"] for row in rows]}
+ finally:
+ await db.close()
+
+
@router.post("/sync")
async def trigger_sync(request: Request):
await get_current_user(request)
diff --git a/app/routers/mood.py b/app/routers/mood.py
index 9b18834..9f65a86 100644
--- a/app/routers/mood.py
+++ b/app/routers/mood.py
@@ -175,6 +175,29 @@ async def get_search_history(request: Request):
await db.close()
+@router.get("/history/shared/{entry_id}")
+async def get_shared_history(entry_id: int):
+ """Public endpoint — returns a history entry by ID (no auth required)."""
+ db = await get_db()
+ try:
+ cursor = await db.execute(
+ "SELECT id, mood, results, meta, created_at FROM search_history WHERE id = ?",
+ (entry_id,),
+ )
+ row = await cursor.fetchone()
+ if not row:
+ raise HTTPException(status_code=404, detail="Search not found")
+ return {
+ "id": row["id"],
+ "mood": row["mood"],
+ "results": json.loads(row["results"]),
+ "meta": json.loads(row["meta"]) if row["meta"] else {},
+ "created_at": row["created_at"],
+ }
+ finally:
+ await db.close()
+
+
@router.delete("/history/{entry_id}")
async def delete_history_entry(entry_id: int, request: Request):
user = await get_current_user(request)
diff --git a/app/static/app.js b/app/static/app.js
index 55e068f..508c173 100644
--- a/app/static/app.js
+++ b/app/static/app.js
@@ -217,6 +217,7 @@ function renderResults(data, append = false) {
data.recommendations.forEach(movie => {
const card = document.createElement('div');
card.className = 'movie-card bg-dark-200 rounded-xl overflow-hidden border border-gray-800';
+ card.dataset.jellyfinId = movie.jellyfin_id;
card.innerHTML = `

r.jellyfin_id));
}
function toggleHistoryVisibility(show) {
@@ -334,6 +337,51 @@ function toggleHistoryVisibility(show) {
}
}
+// --- Watch Check ---
+
+async function markWatchedCards(jellyfinIds) {
+ if (!currentUser || !jellyfinIds.length) return;
+ try {
+ const res = await fetch(`${API}/api/library/watch-check`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ jellyfin_ids: jellyfinIds })
+ });
+ if (!res.ok) return;
+ const { watched } = await res.json();
+ if (!watched.length) return;
+ const watchedSet = new Set(watched);
+ document.querySelectorAll('.movie-card').forEach(card => {
+ if (watchedSet.has(card.dataset.jellyfinId)) {
+ card.classList.add('watched');
+ }
+ });
+ } catch { /* ignore */ }
+}
+
+// --- Shared Search ---
+
+async function loadSharedSearch() {
+ const params = new URLSearchParams(window.location.search);
+ const sharedId = params.get('s');
+ if (!sharedId) return false;
+ try {
+ const res = await fetch(`${API}/api/history/shared/${sharedId}`);
+ if (!res.ok) return false;
+ const entry = await res.json();
+ document.getElementById('mood-input').value = entry.mood;
+ const data = { recommendations: entry.results, meta: entry.meta };
+ renderResults(data);
+ // If logged in, check watch state
+ if (currentUser) {
+ await markWatchedCards(entry.results.map(r => r.jellyfin_id));
+ }
+ return true;
+ } catch {
+ return false;
+ }
+}
+
// --- Library Stats ---
async function loadStats() {
@@ -418,6 +466,20 @@ document.getElementById('reroll-btn').addEventListener('click', () => {
findMovies(shownMovieIds);
});
+// Share / Copy Link
+document.getElementById('share-btn').addEventListener('click', () => {
+ if (!currentHistoryId) return;
+ const url = `${window.location.origin}${window.location.pathname}?s=${currentHistoryId}`;
+ navigator.clipboard.writeText(url).then(() => {
+ const btn = document.getElementById('share-btn');
+ btn.textContent = 'Copied!';
+ setTimeout(() => { btn.textContent = 'Copy Link'; }, 2000);
+ }).catch(() => {
+ // Fallback for insecure contexts
+ prompt('Copy this link:', `${window.location.origin}${window.location.pathname}?s=${currentHistoryId}`);
+ });
+});
+
// Reset
document.getElementById('reset-btn').addEventListener('click', () => {
// Clear mood input and state
@@ -445,3 +507,4 @@ document.getElementById('reset-btn').addEventListener('click', () => {
// --- Init ---
checkAuth();
+loadSharedSearch();
diff --git a/app/static/index.html b/app/static/index.html
index ee6c393..627cb71 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -109,10 +109,13 @@
-
+
+
diff --git a/app/static/styles.css b/app/static/styles.css
index e347cb3..d526746 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -50,3 +50,23 @@
border-color: #22c55e;
color: #86efac;
}
+
+.movie-card.watched {
+ opacity: 0.45;
+}
+.movie-card.watched::after {
+ content: 'Watched';
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ background: rgba(34, 197, 94, 0.8);
+ color: white;
+ font-size: 0.7rem;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 9999px;
+ z-index: 10;
+}
+.movie-card.watched {
+ position: relative;
+}