diff --git a/backend/fetcher.py b/backend/fetcher.py index 516a8ac..20ffc86 100644 --- a/backend/fetcher.py +++ b/backend/fetcher.py @@ -1,4 +1,5 @@ import asyncio +import html import logging import aiosqlite from googleapiclient.discovery import build @@ -77,8 +78,8 @@ async def fetch_subscriptions(): VALUES (?, ?, ?, ?, ?, ?, ?) """, ( vid, - snippet.get("title", "Untitled"), - snippet.get("channelTitle", "Unknown"), + html.unescape(snippet.get("title", "Untitled")), + html.unescape(snippet.get("channelTitle", "Unknown")), snippet.get("channelId", cid), f"https://www.youtube.com/watch?v={vid}", snippet.get("thumbnails", {}).get("high", {}).get("url", ""), diff --git a/frontend/src/routes/YoutubeDashboard.svelte b/frontend/src/routes/YoutubeDashboard.svelte index d79ef8e..7750dec 100644 --- a/frontend/src/routes/YoutubeDashboard.svelte +++ b/frontend/src/routes/YoutubeDashboard.svelte @@ -7,10 +7,12 @@ let isLoading = $state(false); let isSyncing = $state(false); let syncMessage = $state(''); + let pendingCount = $state(0); let theme = $state(localStorage.getItem('theme') || 'dark'); let summaryPanel = $state(null); let notes = $state(JSON.parse(localStorage.getItem('t-yt-notes') || '{}')); let activeNote = $state(null); + let showHelp = $state(false); function toggleTheme() { theme = theme === 'dark' ? 'light' : 'dark'; @@ -59,10 +61,12 @@ } catch (err) { isSyncing = false; syncMessage = ''; } } - async function updateStatus(videoId, newStatus) { + async function updateStatus(videoId, newStatus, silent = false) { await fetch(`/api/videos/${videoId}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }) }); - videos = videos.filter(v => v.video_id !== videoId); - if (selectedVideo?.video_id === videoId) selectedVideo = videos[0] || null; + if (!silent) { + videos = videos.filter(v => v.video_id !== videoId); + if (selectedVideo?.video_id === videoId) selectedVideo = videos[0] || null; + } } async function generateSummary(videoId) { @@ -75,42 +79,95 @@ $effect(() => { if (selectedVideo && summaryPanel) { tick().then(() => summaryPanel.scrollTop = 0); + // Auto-mark as read after 3s of viewing a summarized video + const vid = selectedVideo.video_id; + const hasSummary = selectedVideo.summary && !selectedVideo.summary.includes('Error') && !selectedVideo.summary.includes('📺 Short clip'); + if (hasSummary && activeTab === 'pending') { + const timer = setTimeout(() => updateStatus(vid, 'read', true), 3000); + return () => clearTimeout(timer); + } } }); onMount(() => { document.documentElement.className = theme; loadVideos(); + loadPendingCount(); window.addEventListener('keydown', handleKeydown); return () => window.removeEventListener('keydown', handleKeydown); }); + async function loadPendingCount() { + try { + const res = await fetch('/api/videos?status=pending&limit=1'); + if (res.ok) { + // Count from header or estimate — just ping for now + const all = await fetch('/api/videos?status=pending&limit=200'); + if (all.ok) pendingCount = (await all.json()).length; + } + } catch(e) {} + } + function handleKeydown(e) { if (!videos.length) return; + if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return; const idx = videos.findIndex(v => v.video_id === selectedVideo?.video_id); + let nextIdx = idx; if (e.key === 'j' || e.key === 'ArrowDown') { e.preventDefault(); - const next = idx < videos.length - 1 ? idx + 1 : 0; - selectedVideo = videos[next]; - scrollToVideo(next); + if (idx < videos.length - 1) nextIdx = idx + 1; else return; } else if (e.key === 'k' || e.key === 'ArrowUp') { e.preventDefault(); - const prev = idx > 0 ? idx - 1 : videos.length - 1; - selectedVideo = videos[prev]; - scrollToVideo(prev); + if (idx > 0) nextIdx = idx - 1; else return; } else if (e.key === 'r' || e.key === 'm') { e.preventDefault(); if (selectedVideo) updateStatus(selectedVideo.video_id, 'read'); + return; } else if (e.key === 's') { e.preventDefault(); if (selectedVideo) updateStatus(selectedVideo.video_id, 'skipped'); + return; + } else if (e.key === 'n') { + e.preventDefault(); + if (selectedVideo) activeNote = selectedVideo.video_id; + return; + } else if (e.key === 'Enter') { + e.preventDefault(); + if (activeNote && selectedVideo && activeNote === selectedVideo.video_id) { + localStorage.setItem('t-yt-notes', JSON.stringify(notes)); + activeNote = null; + } else if (selectedVideo) { + updateStatus(selectedVideo.video_id, 'read'); + } + return; + } else if (e.key === '?') { + e.preventDefault(); + showHelp = !showHelp; + return; + } else { + return; } + selectedVideo = videos[nextIdx]; + scrollToVideo(nextIdx); } function scrollToVideo(index) { - requestAnimationFrame(() => { - const el = document.querySelector(`[data-video-index="${index}"]`); - if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - }); + const el = document.querySelector(`[data-video-index="${index}"]`); + if (el) el.scrollIntoView({ block: 'nearest', behavior: 'instant' }); + } + + function autoFocus(node) { + requestAnimationFrame(() => node.focus()); + } + + function saveNote() { + const vid = selectedVideo?.video_id; + if (!vid) return; + const existing = notes[vid] || ''; + if (existing && !existing.startsWith('[')) { + notes[vid] = `[${new Date().toLocaleString()}] ${existing}`; + } + localStorage.setItem('t-yt-notes', JSON.stringify(notes)); + activeNote = null; } function formatMarkdown(text) { @@ -155,7 +212,7 @@ {/each} @@ -164,7 +221,7 @@ {@const i = videos.indexOf(video)} + + + ▶ Watch + + + + + + {#if activeNote === selectedVideo.video_id} +
+ + +
+ {:else if notes[selectedVideo.video_id]} +
activeNote = selectedVideo.video_id} + title="Click to edit">{notes[selectedVideo.video_id]}
+ {/if} + +

AI Digest

{#if !selectedVideo.summary || selectedVideo.summary.includes("Error")} + class="text-[11px] px-3 py-1 font-semibold rounded border transition" style="color:var(--accent);border-color:var(--accent)">Generate {/if}
{@html formatMarkdown(selectedVideo.summary)}
- - -
- -
- {selectedVideo.channel_name} -

{selectedVideo.title}

-
- - - - â–¶ Watch -
- {#if activeNote === selectedVideo.video_id} -
- - -
- {/if} -
-
{:else}
@@ -235,4 +298,21 @@ {/if}
+ + {#if showHelp} +
showHelp = false}> +
e.stopPropagation()}> +

Keyboard Shortcuts

+
+
Navigatej ↓
+
Navigate upk ↑
+
Mark Readr / Enter
+
Skips
+
Noten
+
Save noteEnter / Esc
+
+

Press ? to toggle

+
+
+ {/if}