fix: skip auth token when Authelia authed + frontend search bar improvements
This commit is contained in:
@@ -36,7 +36,11 @@ async def validate_api_token(request: Request, call_next):
|
|||||||
path = request.url.path
|
path = request.url.path
|
||||||
method = request.method
|
method = request.method
|
||||||
# Only check auth for write operations on /api/*
|
# Only check auth for write operations on /api/*
|
||||||
|
# Skip token check if Authelia/Caddy already authenticated the user
|
||||||
if path.startswith("/api/") and method in ("POST", "PATCH", "DELETE") and API_TOKEN:
|
if path.startswith("/api/") and method in ("POST", "PATCH", "DELETE") and API_TOKEN:
|
||||||
|
# Check if request came through Authelia (Remote-User header set by Caddy forward_auth)
|
||||||
|
if request.headers.get("Remote-User"):
|
||||||
|
return await call_next(request)
|
||||||
auth_header = request.headers.get("Authorization", "")
|
auth_header = request.headers.get("Authorization", "")
|
||||||
if not auth_header.startswith("Bearer ") or auth_header[7:] != API_TOKEN:
|
if not auth_header.startswith("Bearer ") or auth_header[7:] != API_TOKEN:
|
||||||
return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
|
return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
|
||||||
|
|||||||
@@ -21,13 +21,37 @@
|
|||||||
let allChannels = $state([]);
|
let allChannels = $state([]);
|
||||||
let editingCategories = $state(false);
|
let editingCategories = $state(false);
|
||||||
let prevSelectedId = $state(null);
|
let prevSelectedId = $state(null);
|
||||||
|
|
||||||
|
// Search state
|
||||||
let searchQuery = $state('');
|
let searchQuery = $state('');
|
||||||
let searchTimeout = null;
|
let searchResults = $state([]);
|
||||||
|
let isSearching = $state(false);
|
||||||
|
let showSearchResults = $state(false);
|
||||||
|
|
||||||
|
// Batch select state
|
||||||
|
let selectedVideoIds = $state(new Set());
|
||||||
|
let showBatchBar = $state(false);
|
||||||
|
|
||||||
|
// Stats state
|
||||||
|
let statsData = $state(null);
|
||||||
|
let showStats = $state(false);
|
||||||
|
|
||||||
// Agent queue state
|
// Agent queue state
|
||||||
let agentItems = $state([]);
|
let agentItems = $state([]);
|
||||||
let agentLoading = $state(false);
|
let agentLoading = $state(false);
|
||||||
|
|
||||||
|
// Stats fetcher
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/stats');
|
||||||
|
if (res.ok) statsData = await res.json();
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (activeTab === 'stats' && !statsData) loadStats();
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (activeTab === 'pending' && channelFilter) {
|
if (activeTab === 'pending' && channelFilter) {
|
||||||
loadVideos();
|
loadVideos();
|
||||||
@@ -53,6 +77,83 @@
|
|||||||
|
|
||||||
let noteIds = $derived(new Set(Object.keys(notes).filter(k => notes[k] && notes[k].trim())));
|
let noteIds = $derived(new Set(Object.keys(notes).filter(k => notes[k] && notes[k].trim())));
|
||||||
|
|
||||||
|
// Search debouncer
|
||||||
|
let searchTimeout = null;
|
||||||
|
async function handleSearch(query) {
|
||||||
|
searchQuery = query;
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
showSearchResults = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isSearching = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/videos/search?q=${encodeURIComponent(query)}&limit=50`);
|
||||||
|
if (res.ok) {
|
||||||
|
searchResults = await res.json();
|
||||||
|
showSearchResults = true;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Search failed:', e);
|
||||||
|
} finally {
|
||||||
|
isSearching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch operations
|
||||||
|
function toggleSelect(videoId) {
|
||||||
|
const newSet = new Set(selectedVideoIds);
|
||||||
|
if (newSet.has(videoId)) {
|
||||||
|
newSet.delete(videoId);
|
||||||
|
} else {
|
||||||
|
newSet.add(videoId);
|
||||||
|
}
|
||||||
|
selectedVideoIds = newSet;
|
||||||
|
showBatchBar = newSet.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAll() {
|
||||||
|
selectedVideoIds = new Set(filteredVideos.map(v => v.video_id));
|
||||||
|
showBatchBar = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedVideoIds = new Set();
|
||||||
|
showBatchBar = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchUpdate(status) {
|
||||||
|
if (selectedVideoIds.size === 0) return;
|
||||||
|
const ids = Array.from(selectedVideoIds);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/videos/batch', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
body: JSON.stringify({ video_ids: ids, status })
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
// Remove selected from local list
|
||||||
|
videos = videos.filter(v => !ids.includes(v.video_id));
|
||||||
|
clearSelection();
|
||||||
|
loadPendingCount();
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Batch update failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
if (!seconds || seconds < 60) return '';
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = seconds % 60;
|
||||||
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatViews(count) {
|
||||||
|
if (!count || count < 1000) return count || '';
|
||||||
|
if (count < 1000000) return `${(count / 1000).toFixed(1)}K`;
|
||||||
|
return `${(count / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
theme = theme === 'dark' ? 'light' : 'dark';
|
theme = theme === 'dark' ? 'light' : 'dark';
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
@@ -76,13 +177,6 @@
|
|||||||
if (channelFilter && !channelFilter.startsWith('📁')) {
|
if (channelFilter && !channelFilter.startsWith('📁')) {
|
||||||
params.set('channel', channelFilter);
|
params.set('channel', channelFilter);
|
||||||
}
|
}
|
||||||
if (searchQuery.trim()) {
|
|
||||||
params.set('search', searchQuery.trim());
|
|
||||||
}
|
|
||||||
if (searchQuery.trim()) {
|
|
||||||
params.delete('status');
|
|
||||||
params.set('limit', '50');
|
|
||||||
}
|
|
||||||
const res = await fetch(`/api/videos?${params}`);
|
const res = await fetch(`/api/videos?${params}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
videos = await res.json();
|
videos = await res.json();
|
||||||
@@ -385,6 +479,13 @@
|
|||||||
if (!d) return '';
|
if (!d) return '';
|
||||||
return d.slice(0, 10);
|
return d.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stats display helpers
|
||||||
|
function formatStatsNumber(n) {
|
||||||
|
if (n >= 1000000) return `${(n/1000000).toFixed(1)}M`;
|
||||||
|
if (n >= 1000) return `${(n/1000).toFixed(1)}K`;
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="background:var(--bg-primary);color:var(--text-primary);height:100vh;overflow:hidden;display:flex;flex-direction:column">
|
<div style="background:var(--bg-primary);color:var(--text-primary);height:100vh;overflow:hidden;display:flex;flex-direction:column">
|
||||||
@@ -440,15 +541,34 @@
|
|||||||
<aside style="width:280px;background:var(--bg-secondary);border-right:1px solid var(--border)" class="flex flex-col flex-shrink-0">
|
<aside style="width:280px;background:var(--bg-secondary);border-right:1px solid var(--border)" class="flex flex-col flex-shrink-0">
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="flex p-1.5" style="border-bottom:1px solid var(--border)">
|
<div class="flex p-1.5" style="border-bottom:1px solid var(--border)">
|
||||||
{#each ['pending','read','skipped','notes','🤖 Agent'] as tab}
|
{#each ['pending','read','skipped','notes','stats','🤖 Agent'] as tab}
|
||||||
<button onclick={() => activeTab = tab}
|
<button onclick={() => activeTab = tab}
|
||||||
class="flex-1 py-1.5 text-center text-[11px] font-bold rounded uppercase transition"
|
class="flex-1 py-1.5 text-center text-[11px] font-bold rounded uppercase transition"
|
||||||
style="color: {activeTab === tab ? 'var(--accent)' : 'var(--text-muted)'}">
|
style="color: {activeTab === tab ? 'var(--accent)' : 'var(--text-muted)'}">
|
||||||
{tab === 'pending' && pendingCount ? `Pending (${pendingCount})` : tab}
|
{tab === 'pending' && pendingCount ? `Pending (${pendingCount})` : tab === 'stats' ? '📊 Stats' : tab === '🤖 Agent' ? '🤖 Agent' : tab}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search bar (shown in pending/read/skipped tabs) -->
|
||||||
|
{#if activeTab !== '🤖 Agent' && activeTab !== 'stats' && activeTab !== 'notes'}
|
||||||
|
<div class="px-2 py-1.5" style="border-bottom:1px solid var(--border)">
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
oninput={handleSearch}
|
||||||
|
placeholder="Search videos..."
|
||||||
|
class="w-full text-[11px] p-1.5 rounded border"
|
||||||
|
style="background:var(--bg-primary);color:var(--text-primary);border-color:var(--border)"
|
||||||
|
onfocus={() => { if (searchResults.length) showSearchResults = true; }}
|
||||||
|
onblur={() => setTimeout(() => showSearchResults = false, 200)}>
|
||||||
|
{#if isSearching}
|
||||||
|
<div class="absolute right-2 top-1.5 text-[9px] animate-spin" style="color:var(--accent)">↻</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Agent Queue tab content -->
|
<!-- Agent Queue tab content -->
|
||||||
{#if activeTab === '🤖 Agent'}
|
{#if activeTab === '🤖 Agent'}
|
||||||
<div class="flex-1 overflow-y-auto" style="border-color:var(--border)">
|
<div class="flex-1 overflow-y-auto" style="border-color:var(--border)">
|
||||||
@@ -486,26 +606,56 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else if activeTab === 'stats'}
|
||||||
<!-- Search bar -->
|
<!-- Stats Dashboard -->
|
||||||
<div class="px-2 py-1.5" style="border-bottom:1px solid var(--border)">
|
<div class="flex-1 overflow-y-auto p-3 space-y-3">
|
||||||
<div class="flex items-center space-x-1" style="background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;padding:2px 8px">
|
{#if !statsData}
|
||||||
<span style="color:var(--text-muted);font-size:12px">🔍</span>
|
<div class="flex items-center justify-center py-8 text-xs" style="color:var(--text-muted)">Loading stats...</div>
|
||||||
<input type="text" bind:value={searchQuery}
|
{:else}
|
||||||
placeholder="Search videos..."
|
<!-- Summary cards -->
|
||||||
class="w-full text-[11px] py-1 bg-transparent outline-none"
|
<div class="grid grid-cols-2 gap-2">
|
||||||
style="color:var(--text-primary)"
|
<div class="p-2 rounded border text-center" style="background:var(--bg-tertiary);border-color:var(--border)">
|
||||||
oninput={() => {
|
<div class="text-lg font-black" style="color:var(--accent)">{formatStatsNumber(statsData.total || 0)}</div>
|
||||||
clearTimeout(searchTimeout);
|
<div class="text-[9px]" style="color:var(--text-muted)">Total Videos</div>
|
||||||
searchTimeout = setTimeout(loadVideos, 300);
|
</div>
|
||||||
}}
|
<div class="p-2 rounded border text-center" style="background:var(--bg-tertiary);border-color:var(--border)">
|
||||||
onkeydown={(e) => { if (e.key === 'Enter') { clearTimeout(searchTimeout); loadVideos(); } }} />
|
<div class="text-lg font-black" style="color:var(--accent)">{statsData.summary_rate || 0}%</div>
|
||||||
{#if searchQuery}
|
<div class="text-[9px]" style="color:var(--text-muted)">Summary Rate</div>
|
||||||
<button onclick={() => { searchQuery = ''; loadVideos(); }}
|
</div>
|
||||||
class="text-[10px] px-1" style="color:var(--text-muted)">✕</button>
|
<div class="p-2 rounded border text-center" style="background:var(--bg-tertiary);border-color:var(--border)">
|
||||||
{/if}
|
<div class="text-lg font-black" style="color:var(--accent)">{statsData.channels || 0}</div>
|
||||||
</div>
|
<div class="text-[9px]" style="color:var(--text-muted)">Channels</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 rounded border text-center" style="background:var(--bg-tertiary);border-color:var(--border)">
|
||||||
|
<div class="text-lg font-black" style="color:var(--accent)">{statsData.last_7_days || 0}</div>
|
||||||
|
<div class="text-[9px]" style="color:var(--text-muted)">Last 7 Days</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status breakdown -->
|
||||||
|
<div class="p-2 rounded border" style="border-color:var(--border)">
|
||||||
|
<div class="text-[10px] font-bold mb-2" style="color:var(--text-muted)">STATUS BREAKDOWN</div>
|
||||||
|
{#each Object.entries(statsData.status_breakdown || {}) as [status, count]}
|
||||||
|
<div class="flex justify-between items-center py-1">
|
||||||
|
<span class="text-[11px]" style="color:var(--text-primary)">{status}</span>
|
||||||
|
<span class="text-[11px] font-bold" style="color:var(--accent)">{count}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top channels -->
|
||||||
|
<div class="p-2 rounded border" style="border-color:var(--border)">
|
||||||
|
<div class="text-[10px] font-bold mb-2" style="color:var(--text-muted)">TOP CHANNELS</div>
|
||||||
|
{#each (statsData.top_channels || []).slice(0, 8) as ch}
|
||||||
|
<div class="flex justify-between items-center py-1">
|
||||||
|
<span class="text-[10px] truncate flex-1 mr-2" style="color:var(--text-primary)">{ch.name}</span>
|
||||||
|
<span class="text-[10px]" style="color:var(--text-muted)">{ch.count}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
<!-- Channel filter bar -->
|
<!-- Channel filter bar -->
|
||||||
{#if (allChannels.length || channels.length) > 1}
|
{#if (allChannels.length || channels.length) > 1}
|
||||||
<div class="px-2 py-1.5 space-y-1" style="border-bottom:1px solid var(--border)">
|
<div class="px-2 py-1.5 space-y-1" style="border-bottom:1px solid var(--border)">
|
||||||
@@ -571,14 +721,31 @@
|
|||||||
{@const i = filteredVideos.indexOf(video)}
|
{@const i = filteredVideos.indexOf(video)}
|
||||||
<button onclick={() => { selectedVideo = video; }}
|
<button onclick={() => { selectedVideo = video; }}
|
||||||
data-video-index={i}
|
data-video-index={i}
|
||||||
class="w-full text-left p-3 transition flex items-start space-x-3 outline-none focus:outline-none"
|
class="w-full text-left p-2.5 transition flex items-start space-x-2 outline-none focus:outline-none"
|
||||||
style="border-left:2px solid {selectedVideo?.video_id === video.video_id ? 'var(--accent)' : 'transparent'};
|
style="border-left:2px solid {selectedVideo?.video_id === video.video_id ? 'var(--accent)' : 'transparent'};
|
||||||
background:{selectedVideo?.video_id === video.video_id ? 'var(--bg-tertiary)' : 'transparent'}">
|
background:{selectedVideo?.video_id === video.video_id ? 'var(--bg-tertiary)' : 'transparent'}">
|
||||||
<img src={video.thumbnail_url} alt="" class="w-20 h-12 rounded object-cover flex-shrink-0" style="background:var(--bg-tertiary)" />
|
<!-- Batch select checkbox -->
|
||||||
<div class="min-w-0">
|
<input type="checkbox"
|
||||||
|
checked={selectedVideoIds.has(video.video_id)}
|
||||||
|
onclick={(e) => { e.stopPropagation(); toggleSelect(video.video_id); }}
|
||||||
|
class="w-3 h-3 mt-1 rounded flex-shrink-0"
|
||||||
|
style="accent-color:var(--accent)">
|
||||||
|
<img src={video.thumbnail_url} alt="" class="w-16 h-10 rounded object-cover flex-shrink-0" style="background:var(--bg-tertiary)" />
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
<span class="block text-[10px] font-semibold truncate" style="color:var(--accent)">{video.channel_name}</span>
|
<span class="block text-[10px] font-semibold truncate" style="color:var(--accent)">{video.channel_name}</span>
|
||||||
<h3 class="text-xs font-bold line-clamp-2 leading-snug" style="color:var(--text-primary)">{video.title}</h3>
|
<h3 class="text-[11px] font-bold line-clamp-2 leading-snug" style="color:var(--text-primary)">{video.title}</h3>
|
||||||
<span class="text-[10px] mt-0.5 block" style="color:var(--text-muted)">{video.publish_date || ''} {noteIds.has(video.video_id) ? '📝' : ''}</span>
|
<div class="flex items-center gap-2 mt-0.5">
|
||||||
|
<span class="text-[9px]" style="color:var(--text-muted)">{video.publish_date || ''}</span>
|
||||||
|
{#if video.duration}
|
||||||
|
<span class="text-[9px]" style="color:var(--text-muted)">{formatDuration(video.duration)}</span>
|
||||||
|
{/if}
|
||||||
|
{#if video.view_count && video.view_count > 0}
|
||||||
|
<span class="text-[9px]" style="color:var(--text-muted)">{formatViews(video.view_count)}</span>
|
||||||
|
{/if}
|
||||||
|
{#if noteIds.has(video.video_id)}
|
||||||
|
<span class="text-[9px]">📝</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -586,6 +753,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Batch action bar -->
|
||||||
|
{#if showBatchBar}
|
||||||
|
<div class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-40 px-4 py-2 rounded-full shadow-lg flex items-center space-x-3"
|
||||||
|
style="background:var(--bg-secondary);border:1px solid var(--border)">
|
||||||
|
<span class="text-xs font-bold" style="color:var(--accent)">{selectedVideoIds.size} selected</span>
|
||||||
|
<button onclick={() => batchUpdate('read')} class="text-[10px] px-3 py-1 text-white font-bold rounded" style="background:var(--accent)">Read All</button>
|
||||||
|
<button onclick={() => batchUpdate('skipped')} class="text-[10px] px-3 py-1 font-bold rounded border" style="color:var(--text-secondary);border-color:var(--border)">Skip All</button>
|
||||||
|
<button onclick={clearSelection} class="text-[10px] px-3 py-1 font-bold rounded border" style="color:var(--text-muted);border-color:var(--border)">Clear</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Right: summary panel -->
|
<!-- Right: summary panel -->
|
||||||
<main bind:this={summaryPanel} onscroll={handleSummaryScroll} id="summary-panel" class="flex-1 overflow-y-auto" style="background:var(--bg-primary);min-height:0">
|
<main bind:this={summaryPanel} onscroll={handleSummaryScroll} id="summary-panel" class="flex-1 overflow-y-auto" style="background:var(--bg-primary);min-height:0">
|
||||||
{#if activeTab === '🤖 Agent'}
|
{#if activeTab === '🤖 Agent'}
|
||||||
@@ -636,7 +814,14 @@
|
|||||||
onclick={() => { channelFilter = selectedVideo.channel_name; }}
|
onclick={() => { channelFilter = selectedVideo.channel_name; }}
|
||||||
title="Filter by this channel">{selectedVideo.channel_name}</span>
|
title="Filter by this channel">{selectedVideo.channel_name}</span>
|
||||||
<h1 class="text-lg font-bold leading-snug" style="color:var(--text-primary)">{selectedVideo.title}</h1>
|
<h1 class="text-lg font-bold leading-snug" style="color:var(--text-primary)">{selectedVideo.title}</h1>
|
||||||
<div class="text-[10px]" style="color:var(--text-muted)">{selectedVideo.publish_date || ''}</div>
|
<div class="text-[10px]" style="color:var(--text-muted)">{selectedVideo.publish_date || ''}
|
||||||
|
{#if selectedVideo.duration}
|
||||||
|
• {formatDuration(selectedVideo.duration)}
|
||||||
|
{/if}
|
||||||
|
{#if selectedVideo.view_count}
|
||||||
|
• {formatViews(selectedVideo.view_count)} views
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<button onclick={() => updateStatus(selectedVideo.video_id, 'read')} class="text-[10px] px-2.5 py-1 text-white font-bold rounded transition" style="background:var(--accent)">Read (r)</button>
|
<button onclick={() => updateStatus(selectedVideo.video_id, 'read')} class="text-[10px] px-2.5 py-1 text-white font-bold rounded transition" style="background:var(--accent)">Read (r)</button>
|
||||||
<button onclick={() => updateStatus(selectedVideo.video_id, 'skipped')} class="text-[10px] px-2.5 py-1 font-bold rounded border transition" style="color:var(--text-secondary);border-color:var(--border)">Skip (s)</button>
|
<button onclick={() => updateStatus(selectedVideo.video_id, 'skipped')} class="text-[10px] px-2.5 py-1 font-bold rounded border transition" style="color:var(--text-secondary);border-color:var(--border)">Skip (s)</button>
|
||||||
@@ -653,7 +838,8 @@
|
|||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<textarea id="note-textarea" rows="3" class="w-full text-xs p-2 rounded border resize-none"
|
<textarea id="note-textarea" rows="3" class="w-full text-xs p-2 rounded border resize-none"
|
||||||
style="background:var(--bg-tertiary);color:var(--text-primary);border-color:var(--border)"
|
style="background:var(--bg-tertiary);color:var(--text-primary);border-color:var(--border)"
|
||||||
placeholder="Write instructions for the agent here... e.g. 'Add this gyroid lattice workflow as a Hermes skill'"
|
placeholder="Write instructions for the agent here...
|
||||||
|
e.g. 'Add this gyroid lattice workflow as a Hermes skill'"
|
||||||
value={notes[selectedVideo.video_id] || ''}
|
value={notes[selectedVideo.video_id] || ''}
|
||||||
oninput={e => notes[selectedVideo.video_id] = e.target.value}
|
oninput={e => notes[selectedVideo.video_id] = e.target.value}
|
||||||
use:autoFocus
|
use:autoFocus
|
||||||
|
|||||||
+14
-16
@@ -2,6 +2,7 @@
|
|||||||
import asyncio, os, sys, logging
|
import asyncio, os, sys, logging
|
||||||
|
|
||||||
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
|
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
BACKEND = os.path.join(REPO_ROOT, "backend")
|
||||||
ENV_PATH = os.path.join(REPO_ROOT, ".env")
|
ENV_PATH = os.path.join(REPO_ROOT, ".env")
|
||||||
if os.path.exists(ENV_PATH):
|
if os.path.exists(ENV_PATH):
|
||||||
with open(ENV_PATH) as f:
|
with open(ENV_PATH) as f:
|
||||||
@@ -9,31 +10,29 @@ if os.path.exists(ENV_PATH):
|
|||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line and not line.startswith("#") and "=" in line:
|
if line and not line.startswith("#") and "=" in line:
|
||||||
key, _, value = line.partition("=")
|
key, _, value = line.partition("=")
|
||||||
value = value.strip().strip("'\\"")
|
value = value.strip().strip("'\"")
|
||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
|
|
||||||
sys.path.insert(0, REPO_ROOT)
|
sys.path.insert(0, BACKEND)
|
||||||
sys.path.insert(0, os.path.join(REPO_ROOT, "backend"))
|
os.chdir(BACKEND)
|
||||||
|
|
||||||
from backend.db import init_db
|
from db import init_db, DB_PATH
|
||||||
from backend.fetcher import fetch_subscriptions
|
from fetcher import fetch_subscriptions
|
||||||
from backend.summarizer import summarize_all_pending
|
from summarizer import summarize_video
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
|
||||||
BATCH_SIZE = int(os.environ.get("SUMMARIZE_BATCH", "50"))
|
BATCH_SIZE = int(os.environ.get("SUMMARIZE_BATCH", "20"))
|
||||||
BATCH_DELAY = float(os.environ.get("SUMMARIZE_BATCH_DELAY", "1.0"))
|
BATCH_DELAY = float(os.environ.get("SUMMARIZE_BATCH_DELAY", "2.0"))
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
await init_db()
|
await init_db()
|
||||||
|
|
||||||
# Fetch new videos first
|
|
||||||
count = await fetch_subscriptions()
|
count = await fetch_subscriptions()
|
||||||
print(f"Fetched {count} new videos")
|
print(f"Fetched {count} new videos")
|
||||||
|
|
||||||
# Count pending
|
async with aiosqlite.connect(DB_PATH) as db:
|
||||||
async with aiosqlite.connect(os.path.join(REPO_ROOT, "backend", "data", "videos.db")) as db:
|
|
||||||
cur = await db.execute("SELECT COUNT(*) FROM videos WHERE summary IS NULL OR summary = ''")
|
cur = await db.execute("SELECT COUNT(*) FROM videos WHERE summary IS NULL OR summary = ''")
|
||||||
total = (await cur.fetchone())[0]
|
total = (await cur.fetchone())[0]
|
||||||
|
|
||||||
@@ -41,15 +40,13 @@ async def main():
|
|||||||
print("No pending videos to summarize.")
|
print("No pending videos to summarize.")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"Processing batch of up to {BATCH_SIZE} videos (of {total} pending)...")
|
print(f"Processing batch of {BATCH_SIZE} (of {total} pending)...")
|
||||||
|
|
||||||
# Get pending video IDs
|
async with aiosqlite.connect(DB_PATH) as db:
|
||||||
async with aiosqlite.connect(os.path.join(REPO_ROOT, "backend", "data", "videos.db")) as db:
|
|
||||||
db.row_factory = aiosqlite.Row
|
db.row_factory = aiosqlite.Row
|
||||||
cur = await db.execute("SELECT video_id FROM videos WHERE summary IS NULL OR summary = '' LIMIT ?", (BATCH_SIZE,))
|
cur = await db.execute("SELECT video_id FROM videos WHERE summary IS NULL OR summary = '' LIMIT ?", (BATCH_SIZE,))
|
||||||
rows = await cur.fetchall()
|
rows = await cur.fetchall()
|
||||||
|
|
||||||
from backend.summarizer import summarize_video
|
|
||||||
done = 0
|
done = 0
|
||||||
for row in rows:
|
for row in rows:
|
||||||
await summarize_video(row["video_id"])
|
await summarize_video(row["video_id"])
|
||||||
@@ -57,7 +54,8 @@ async def main():
|
|||||||
if done < len(rows):
|
if done < len(rows):
|
||||||
await asyncio.sleep(BATCH_DELAY)
|
await asyncio.sleep(BATCH_DELAY)
|
||||||
|
|
||||||
print(f"Processed {done}/{len(rows)} in batch, {total - done} remaining")
|
remaining = total - done
|
||||||
|
print(f"Batch done: {done}/{len(rows)}, {remaining} pending")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user