diff --git a/backend/main.py b/backend/main.py index 0fe6522..1596570 100644 --- a/backend/main.py +++ b/backend/main.py @@ -37,6 +37,21 @@ async def startup(): async def health(): return {"status": "ok"} +@app.get("/auto_categories.json") +async def auto_categories(): + import json, os + path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend", "src", "auto_categories.json") + if os.path.exists(path): + with open(path) as f: + return json.load(f) + return {} + +@app.get("/api/channels") +async def get_channels(db: aiosqlite.Connection = Depends(get_db)): + cursor = await db.execute("SELECT DISTINCT channel_name, channel_id FROM videos ORDER BY channel_name") + rows = await cursor.fetchall() + return [dict(row) for row in rows] + @app.get("/") async def root(): from fastapi.responses import FileResponse @@ -46,10 +61,15 @@ async def root(): async def get_videos( status: Optional[str] = "pending", limit: int = 50, + channel: Optional[str] = None, db: aiosqlite.Connection = Depends(get_db) ): - query = "SELECT * FROM videos WHERE status = ? ORDER BY publish_date DESC, created_at DESC LIMIT ?" - cursor = await db.execute(query, (status, limit)) + if channel: + query = "SELECT * FROM videos WHERE status = ? AND channel_name = ? ORDER BY publish_date DESC, created_at DESC LIMIT ?" + cursor = await db.execute(query, (status, channel, limit)) + else: + query = "SELECT * FROM videos WHERE status = ? ORDER BY publish_date DESC, created_at DESC LIMIT ?" + cursor = await db.execute(query, (status, limit)) rows = await cursor.fetchall() return [dict(row) for row in rows] diff --git a/frontend/src/auto_categories.js b/frontend/src/auto_categories.js new file mode 100644 index 0000000..5656e69 --- /dev/null +++ b/frontend/src/auto_categories.js @@ -0,0 +1 @@ +export default {"AI Engineer": "AI & Tech", "AI-Fan AI研究室-帆哥": "AI & Tech", "AI大模型": "AI & Tech", "AI探索与发现": "AI & Tech", "Aurora Tech": "AI & Tech", "Claude": "AI & Tech", "ColdFusion": "AI & Tech", "Data Is Beautiful": "AI & Tech", "DeepLearningAI": "AI & Tech", "Google": "AI & Tech", "Google Cloud Tech": "AI & Tech", "Lex Fridman": "AI & Tech", "NiceKate AI": "AI & Tech", "PC Sim": "AI & Tech", "Stanford Online": "AI & Tech", "The Linux Foundation": "AI & Tech", "回到Axton": "AI & Tech", "回形针PaperClip": "AI & Tech", "林亦LYi": "AI & Tech", "硅谷101播客": "AI & Tech", "老石谈芯": "AI & Tech", "鲲鹏Talk": "AI & Tech", "3D Beast": "3D & Design", "Arrimus3D Modeling and Design": "3D & Design", "Blender": "3D & Design", "Blender Guru": "3D & Design", "Creators 3D": "3D & Design", "Designfusion": "3D & Design", "Foundry": "3D & Design", "Grant Abbitt (Gabbitt)": "3D & Design", "Gravity Sketch": "3D & Design", "Hyperganic": "3D & Design", "MSLattice": "3D & Design", "Manolo Remiddi": "3D & Design", "Rhino 3D (Rhinoceros3d official)": "3D & Design", "Shaper3d": "3D & Design", "nTop": "3D & Design", "spherene AG": "3D & Design", "魔界造物": "3D & Design", "Chuck Severance": "Programming", "Coursera": "Programming", "freeCodeCamp.org": "Programming", "Kevin Stratvert": "Programming", "Leila Gharani": "Programming", "LinkedIn Learning": "Programming", "Metabase": "Programming", "n8n": "Programming", "The Coding Gopher": "Programming", "Brian Casel": "Programming", "code秘密花园": "Programming", "cs tech": "Programming", "fequalsf": "Programming", "openscreencast": "Programming", "技术爬爬虾 TechShrimp": "Programming", "林本兔Limbuntu": "Programming", "Best Choice Review": "Hardware & Reviews", "Marques Brownlee": "Hardware & Reviews", "TESTV": "Hardware & Reviews", "先看评测": "Hardware & Reviews", "极客湾Geekerwan": "Hardware & Reviews", "硬核拆解": "Hardware & Reviews", "笔吧评测室": "Hardware & Reviews", "差评硬件部": "Hardware & Reviews", "充电头网": "Hardware & Reviews", "Apple Developer": "Apple Dev", "Made by Google": "Apple Dev", "Allen的分享": "Apple Dev", "Sypnotix": "Apple Dev", "8K World": "Photography", "Can Fiona": "Photography", "小天fotos": "Photography", "影像极客Fotogeeker": "Photography", "摄影师PHiL": "Photography", "虚空光影CosmosFilm": "Photography", "路易斯 LouisDrone": "Photography", "Bruno Mars": "Music", "GraceLeeMusic": "Music", "Katie Melua": "Music", "The Weeknd": "Music", "TheWeekndVEVO": "Music", "杰威爾歌詞MV頻道JVR Lyric MV": "Music", "薛汀哲": "Music", "Salah Trainer": "Sports", "Skills N Talents (swimming)": "Sports", "TotalSports TV": "Sports", "WorldSBK": "Sports", "Everyday Cycling": "Sports", "爱羽客羽毛球网": "Sports", "adidas": "Sports", "Sneaks & Feet极客鞋谈": "Automotive", "TOP的遥控玩具": "Automotive", "悟空的日常": "Automotive"} \ No newline at end of file diff --git a/frontend/src/auto_categories.json b/frontend/src/auto_categories.json new file mode 100644 index 0000000..c624d67 --- /dev/null +++ b/frontend/src/auto_categories.json @@ -0,0 +1 @@ +{"AI Engineer": "AI & Tech", "AI-Fan AI\u7814\u7a76\u5ba4-\u5e06\u54e5": "AI & Tech", "AI\u5927\u6a21\u578b": "AI & Tech", "AI\u63a2\u7d22\u4e0e\u53d1\u73b0": "AI & Tech", "Aurora Tech": "AI & Tech", "Claude": "AI & Tech", "ColdFusion": "AI & Tech", "Data Is Beautiful": "AI & Tech", "DeepLearningAI": "AI & Tech", "Google": "AI & Tech", "Google Cloud Tech": "AI & Tech", "Lex Fridman": "AI & Tech", "NiceKate AI": "AI & Tech", "PC Sim": "AI & Tech", "Stanford Online": "AI & Tech", "The Linux Foundation": "AI & Tech", "\u56de\u5230Axton": "AI & Tech", "\u56de\u5f62\u9488PaperClip": "AI & Tech", "\u6797\u4ea6LYi": "AI & Tech", "\u7845\u8c37101\u64ad\u5ba2": "AI & Tech", "\u8001\u77f3\u8c08\u82af": "AI & Tech", "\u9cb2\u9e4fTalk": "AI & Tech", "3D Beast": "3D & Design", "Arrimus3D Modeling and Design": "3D & Design", "Blender": "3D & Design", "Blender Guru": "3D & Design", "Creators 3D": "3D & Design", "Designfusion": "3D & Design", "Foundry": "3D & Design", "Grant Abbitt (Gabbitt)": "3D & Design", "Gravity Sketch": "3D & Design", "Hyperganic": "3D & Design", "MSLattice": "3D & Design", "Manolo Remiddi": "3D & Design", "Rhino 3D (Rhinoceros3d official)": "3D & Design", "Shaper3d": "3D & Design", "nTop": "3D & Design", "spherene AG": "3D & Design", "\u9b54\u754c\u9020\u7269": "3D & Design", "Chuck Severance": "Programming", "Coursera": "Programming", "freeCodeCamp.org": "Programming", "Kevin Stratvert": "Programming", "Leila Gharani": "Programming", "LinkedIn Learning": "Programming", "Metabase": "Programming", "n8n": "Programming", "The Coding Gopher": "Programming", "Brian Casel": "Programming", "code\u79d8\u5bc6\u82b1\u56ed": "Programming", "cs tech": "Programming", "fequalsf": "Programming", "openscreencast": "Programming", "\u6280\u672f\u722c\u722c\u867e TechShrimp": "Programming", "\u6797\u672c\u5154Limbuntu": "Programming", "Best Choice Review": "Hardware & Reviews", "Marques Brownlee": "Hardware & Reviews", "TESTV": "Hardware & Reviews", "\u5148\u770b\u8bc4\u6d4b": "Hardware & Reviews", "\u6781\u5ba2\u6e7eGeekerwan": "Hardware & Reviews", "\u786c\u6838\u62c6\u89e3": "Hardware & Reviews", "\u7b14\u5427\u8bc4\u6d4b\u5ba4": "Hardware & Reviews", "\u5dee\u8bc4\u786c\u4ef6\u90e8": "Hardware & Reviews", "\u5145\u7535\u5934\u7f51": "Hardware & Reviews", "Apple Developer": "Apple Dev", "Made by Google": "Apple Dev", "Allen\u7684\u5206\u4eab": "Apple Dev", "Sypnotix": "Apple Dev", "8K World": "Photography", "Can Fiona": "Photography", "\u5c0f\u5929fotos": "Photography", "\u5f71\u50cf\u6781\u5ba2Fotogeeker": "Photography", "\u6444\u5f71\u5e08PHiL": "Photography", "\u865a\u7a7a\u5149\u5f71CosmosFilm": "Photography", "\u8def\u6613\u65af LouisDrone": "Photography", "Bruno Mars": "Music", "GraceLeeMusic": "Music", "Katie Melua": "Music", "The Weeknd": "Music", "TheWeekndVEVO": "Music", "\u6770\u5a01\u723e\u6b4c\u8a5eMV\u983b\u9053JVR Lyric MV": "Music", "\u859b\u6c40\u54f2": "Music", "Salah Trainer": "Sports", "Skills N Talents (swimming)": "Sports", "TotalSports TV": "Sports", "WorldSBK": "Sports", "Everyday Cycling": "Sports", "\u7231\u7fbd\u5ba2\u7fbd\u6bdb\u7403\u7f51": "Sports", "adidas": "Sports", "Sneaks & Feet\u6781\u5ba2\u978b\u8c08": "Automotive", "TOP\u7684\u9065\u63a7\u73a9\u5177": "Automotive", "\u609f\u7a7a\u7684\u65e5\u5e38": "Automotive"} \ No newline at end of file diff --git a/frontend/src/routes/YoutubeDashboard.svelte b/frontend/src/routes/YoutubeDashboard.svelte index 120c4ab..59c8a66 100644 --- a/frontend/src/routes/YoutubeDashboard.svelte +++ b/frontend/src/routes/YoutubeDashboard.svelte @@ -14,6 +14,32 @@ let notes = $state(JSON.parse(localStorage.getItem('t-yt-notes') || '{}')); let activeNote = $state(null); let showHelp = $state(false); + let channelFilter = $state(''); + let allChannels = $state([]); + let editingCategories = $state(false); + + $effect(() => { + if (activeTab === 'pending' && channelFilter) { + loadVideos(); + } else if (activeTab === 'pending' && !channelFilter) { + loadVideos(); + } + }); + + // Auto-generated categories (AI-categorized) + const AUTO_CATEGORIES = {"AI Engineer":"AI & Tech","AI-Fan AI研究室-帆哥":"AI & Tech","AI大模型":"AI & Tech","AI探索与发现":"AI & Tech","Aurora Tech":"AI & Tech","Claude":"AI & Tech","ColdFusion":"AI & Tech","Data Is Beautiful":"AI & Tech","DeepLearningAI":"AI & Tech","Google":"AI & Tech","Google Cloud Tech":"AI & Tech","Lex Fridman":"AI & Tech","NiceKate AI":"AI & Tech","PC Sim":"AI & Tech","Stanford Online":"AI & Tech","The Linux Foundation":"AI & Tech","回到Axton":"AI & Tech","回形针PaperClip":"AI & Tech","林亦LYi":"AI & Tech","硅谷101播客":"AI & Tech","老石谈芯":"AI & Tech","鲲鹏Talk":"AI & Tech","3D Beast":"3D & Design","Arrimus3D Modeling and Design":"3D & Design","Blender":"3D & Design","Blender Guru":"3D & Design","Creators 3D":"3D & Design","Designfusion":"3D & Design","Foundry":"3D & Design","Grant Abbitt (Gabbitt)":"3D & Design","Gravity Sketch":"3D & Design","Hyperganic":"3D & Design","MSLattice":"3D & Design","Manolo Remiddi":"3D & Design","Rhino 3D (Rhinoceros3d official)":"3D & Design","Shaper3d":"3D & Design","nTop":"3D & Design","spherene AG":"3D & Design","魔界造物":"3D & Design","Chuck Severance":"Programming","Coursera":"Programming","freeCodeCamp.org":"Programming","Kevin Stratvert":"Programming","Leila Gharani":"Programming","LinkedIn Learning":"Programming","Metabase":"Programming","n8n":"Programming","The Coding Gopher":"Programming","Brian Casel":"Programming","code秘密花园":"Programming","cs tech":"Programming","fequalsf":"Programming","openscreencast":"Programming","技术爬爬虾 TechShrimp":"Programming","林本兔Limbuntu":"Programming","Best Choice Review":"Hardware & Reviews","Marques Brownlee":"Hardware & Reviews","TESTV":"Hardware & Reviews","先看评测":"Hardware & Reviews","极客湾Geekerwan":"Hardware & Reviews","硬核拆解":"Hardware & Reviews","笔吧评测室":"Hardware & Reviews","差评硬件部":"Hardware & Reviews","充电头网":"Hardware & Reviews","Apple Developer":"Apple Dev","Made by Google":"Apple Dev","Allen的分享":"Apple Dev","Sypnotix":"Apple Dev","8K World":"Photography","Can Fiona":"Photography","小天fotos":"Photography","影像极客Fotogeeker":"Photography","摄影师PHiL":"Photography","虚空光影CosmosFilm":"Photography","路易斯 LouisDrone":"Photography","Bruno Mars":"Music","GraceLeeMusic":"Music","Katie Melua":"Music","The Weeknd":"Music","TheWeekndVEVO":"Music","杰威爾歌詞MV頻道JVR Lyric MV":"Music","薛汀哲":"Music","Salah Trainer":"Sports","Skills N Talents (swimming)":"Sports","TotalSports TV":"Sports","WorldSBK":"Sports","Everyday Cycling":"Sports","爱羽客羽毛球网":"Sports","adidas":"Sports","Sneaks & Feet极客鞋谈":"Automotive","TOP的遥控玩具":"Automotive","悟空的日常":"Automotive"}; + + let categories = $state({...AUTO_CATEGORIES}); + + let channels = $derived([...new Set(videos.map(v => v.channel_name))].sort()); + let filteredVideos = $derived( + channelFilter + ? channelFilter.startsWith('📁') + ? videos.filter(v => (categories[v.channel_name] || '') === channelFilter.replace('📁 ', '')) + : videos.filter(v => v.channel_name === channelFilter) + : videos + ); + let catList = $derived([...new Set(Object.values(categories).flat())].sort()); function toggleTheme() { theme = theme === 'dark' ? 'light' : 'dark'; @@ -34,7 +60,11 @@ else selectedVideo = null; } } else { - const res = await fetch(`/api/videos?status=${activeTab}&limit=50`); + const params = new URLSearchParams({status: activeTab, limit: '200'}); + if (channelFilter && !channelFilter.startsWith('📁')) { + params.set('channel', channelFilter); + } + const res = await fetch(`/api/videos?${params}`); if (res.ok) { videos = await res.json(); if (videos.length > 0) { @@ -67,6 +97,8 @@ if (!silent) { videos = videos.filter(v => v.video_id !== videoId); if (selectedVideo?.video_id === videoId) selectedVideo = videos[0] || null; + // Reload when list gets short + if (videos.length < 20) loadVideos(); } } @@ -94,6 +126,7 @@ loadVideos(); loadPendingCount(); updateSyncTimer(); + loadChannels(); window.addEventListener('keydown', handleKeydown); return () => window.removeEventListener('keydown', handleKeydown); }); @@ -114,23 +147,29 @@ async function loadPendingCount() { try { - const res = await fetch('/api/videos?status=pending&limit=1'); + const res = await fetch('/api/videos?status=pending&limit=201'); 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; + const all = await res.json(); + pendingCount = all.length >= 201 ? '200+' : all.length; } } catch(e) {} } + async function loadChannels() { + try { + const res = await fetch('/api/channels'); + if (res.ok) allChannels = await res.json(); + } 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); + const idx = filteredVideos.findIndex(v => v.video_id === selectedVideo?.video_id); let nextIdx = idx; if (e.key === 'j' || e.key === 'ArrowDown') { e.preventDefault(); - if (idx < videos.length - 1) nextIdx = idx + 1; else return; + if (idx < filteredVideos.length - 1) nextIdx = idx + 1; else return; } else if (e.key === 'k' || e.key === 'ArrowUp') { e.preventDefault(); if (idx > 0) nextIdx = idx - 1; else return; @@ -162,7 +201,7 @@ } else { return; } - selectedVideo = videos[nextIdx]; + selectedVideo = filteredVideos[nextIdx]; scrollToVideo(nextIdx); } @@ -235,9 +274,67 @@ {/each} + {#if (allChannels.length || channels.length) > 1} +