diff --git a/ta_symlink.py b/ta_symlink.py index d614daf..a6ded67 100644 --- a/ta_symlink.py +++ b/ta_symlink.py @@ -1247,6 +1247,42 @@ def api_recovery_delete(): log(f"❌ Delete failed: {e}") return jsonify({"error": str(e)}), 500 +@app.route("/api/channels", methods=["GET"]) +@requires_auth +def api_get_channels(): + with get_db() as conn: + rows = conn.execute("SELECT DISTINCT channel FROM videos WHERE channel IS NOT NULL ORDER BY channel ASC").fetchall() + channels = [row['channel'] for row in rows if row['channel']] + return jsonify(channels) + +@app.route("/api/channels/videos", methods=["GET"]) +@requires_auth +def api_get_channel_videos(): + channel_name = request.args.get('channel') + if not channel_name: + return jsonify({"error": "No channel name provided"}), 400 + + # Refresh metadata to get filesystem paths + video_map = fetch_all_metadata() + + with get_db() as conn: + rows = conn.execute("SELECT video_id, title, symlink FROM videos WHERE channel = ? ORDER BY published DESC", (channel_name,)).fetchall() + + videos = [] + for row in rows: + vid_id = row['video_id'] + meta = video_map.get(vid_id, {}) + videos.append({ + "video_id": vid_id, + "title": row['title'], + "path": row['symlink'], + "filename": Path(row['symlink']).name if row['symlink'] else meta.get('title'), + "source_path": meta.get('filesystem_path'), + "ta_source": meta.get('channel_name', channel_name) + }) + + return jsonify(videos) + @app.route('/api/recovery/force', methods=['POST']) @requires_auth def api_recovery_force(): diff --git a/ui/src/components/RecoveryModal.svelte b/ui/src/components/RecoveryModal.svelte index 6203a62..684fbd0 100644 --- a/ui/src/components/RecoveryModal.svelte +++ b/ui/src/components/RecoveryModal.svelte @@ -2,7 +2,7 @@ import { createEventDispatcher, onMount, onDestroy } from "svelte"; const dispatch = createEventDispatcher(); - let activeTab: "unindexed" | "rescue" | "redundant" | "lost" = "unindexed"; + let activeTab: "unindexed" | "rescue" | "redundant" | "lost" | "channels" = "unindexed"; let scanning = false; let status = "idle"; let results: any = { unindexed: [], rescue: [], redundant: [], lost: [] }; @@ -12,6 +12,13 @@ let destructMode = false; let selectedPaths = new Set(); + // Channel specific state + let channels: string[] = []; + let selectedChannel = ""; + let channelVideos: any[] = []; + let loadingChannels = false; + let searchingVideos = false; + async function startScan() { scanning = true; try { @@ -47,6 +54,33 @@ }, 2000); } + async function fetchChannels() { + loadingChannels = true; + try { + const res = await fetch("/api/channels"); + channels = await res.json(); + } catch (e) { + console.error("Failed to fetch channels", e); + } finally { + loadingChannels = false; + } + } + + async function fetchChannelVideos(channel: string) { + if (!channel) return; + searchingVideos = true; + selectedChannel = channel; + selectedPaths = new Set(); + try { + const res = await fetch(`/api/channels/videos?channel=${encodeURIComponent(channel)}`); + channelVideos = await res.json(); + } catch (e) { + console.error("Failed to fetch channel videos", e); + } finally { + searchingVideos = false; + } + } + onDestroy(() => { if (pollInterval) clearInterval(pollInterval); }); @@ -62,7 +96,8 @@ const d = await res.json(); if (!isBatch) { alert(d.message); - startScan(); + if (activeTab === "channels") fetchChannelVideos(selectedChannel); + else startScan(); } } catch (e) { alert(e); @@ -91,7 +126,8 @@ const d = await res.json(); if (d.success_count > 0) { alert("Deleted."); - startScan(); + if (activeTab === "channels") fetchChannelVideos(selectedChannel); + else startScan(); } else alert("Error deleting file."); } catch (e) { alert(e); @@ -121,7 +157,8 @@ const d = await res.json(); alert(`Batch complete. Success: ${d.success_count}, Failed: ${d.fail_count}`); selectedPaths = new Set(); - startScan(); + if (activeTab === "channels") fetchChannelVideos(selectedChannel); + else startScan(); } catch (e) { alert("Batch delete failed: " + e); } @@ -137,7 +174,7 @@ } function toggleAll() { - const items = results[activeTab] || []; + const items = activeTab === "channels" ? channelVideos : (results[activeTab] || []); if (selectedPaths.size === items.length && items.length > 0) { selectedPaths = new Set(); } else { @@ -145,7 +182,11 @@ } } - $: allSelected = results[activeTab]?.length > 0 && selectedPaths.size === results[activeTab]?.length; + $: allSelected = (activeTab === "channels" ? channelVideos : results[activeTab])?.length > 0 && selectedPaths.size === (activeTab === "channels" ? channelVideos : results[activeTab])?.length; + + onMount(() => { + fetchChannels(); + });
- {#each ["unindexed", "rescue", "redundant", "lost"] as tab} + {#each ["unindexed", "rescue", "redundant", "lost", "channels"] as tab} {/each}
- {#if scanning && (!results[activeTab] || results[activeTab].length === 0)} + {#if activeTab === "channels"} +
+ +
+
+ + +
+ +
+ + +
+ {#if searchingVideos} +
+ Fetching videos... +
+ {:else if channelVideos.length > 0} + + + + + + + + + + + {#each channelVideos as item} + + + + + + + {/each} + +
+ + Video IDTitleAction
+ toggleSelect(item.path)} + class="accent-neon-cyan" + > + {item.video_id}{item.title} + +
+ {:else} +
+ + Select a channel to view its videos. +
+ {/if} +
+
+ {:else if scanning && (!results[activeTab] || results[activeTab].length === 0)}