(function () { const STORAGE_KEY = "rename_mvp_session_id"; const sessionId = initSessionId(); const state = { sessionId, selectedSeries: null, episodes: [], roots: [], folders: [], discoveredFiles: [], currentSubpath: "", }; const el = { sessionMeta: document.getElementById("sessionMeta"), outputBox: document.getElementById("outputBox"), searchInput: document.getElementById("searchInput"), searchBtn: document.getElementById("searchBtn"), searchResults: document.getElementById("searchResults"), seriesInfo: document.getElementById("seriesInfo"), refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"), episodesList: document.getElementById("episodesList"), refreshSelectedEpisodesBtn: document.getElementById("refreshSelectedEpisodesBtn"), clearSelectedEpisodesBtn: document.getElementById("clearSelectedEpisodesBtn"), selectedEpisodesList: document.getElementById("selectedEpisodesList"), rootsSelect: document.getElementById("rootsSelect"), foldersSelect: document.getElementById("foldersSelect"), refreshRootsBtn: document.getElementById("refreshRootsBtn"), subpathInput: document.getElementById("subpathInput"), loadFoldersBtn: document.getElementById("loadFoldersBtn"), loadFilesBtn: document.getElementById("loadFilesBtn"), recursiveInput: document.getElementById("recursiveInput"), discoveredFilesList: document.getElementById("discoveredFilesList"), refreshSelectedFilesBtn: document.getElementById("refreshSelectedFilesBtn"), clearSelectedFilesBtn: document.getElementById("clearSelectedFilesBtn"), selectedFilesList: document.getElementById("selectedFilesList"), mappingPreviewBtn: document.getElementById("mappingPreviewBtn"), filenamePreviewBtn: document.getElementById("filenamePreviewBtn"), renameExecuteBtn: document.getElementById("renameExecuteBtn"), }; function initSessionId() { const existing = localStorage.getItem(STORAGE_KEY); if (existing) return existing; const created = "ui-" + Date.now() + "-" + Math.floor(Math.random() * 10000); localStorage.setItem(STORAGE_KEY, created); return created; } function q(path) { return path.includes("?") ? `${path}&session_id=${encodeURIComponent(state.sessionId)}` : `${path}?session_id=${encodeURIComponent(state.sessionId)}`; } async function api(path, options = {}) { const resp = await fetch(path, options); const text = await resp.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (_err) { data = { raw: text }; } if (!resp.ok) { throw new Error(data.detail || data.raw || `HTTP ${resp.status}`); } return data; } function out(label, payload) { el.outputBox.textContent = `${label}\n${JSON.stringify(payload, null, 2)}`; } function makeActionButton(label, handler, secondary) { const btn = document.createElement("button"); btn.textContent = label; if (secondary) btn.className = "secondary"; btn.addEventListener("click", handler); return btn; } function setLoading(btn, loading) { if (!btn) return; btn.disabled = loading; } async function loadRoots() { const data = await api("/api/files/roots"); state.roots = data.items || []; el.rootsSelect.innerHTML = ""; for (const root of state.roots) { const opt = document.createElement("option"); opt.value = root.id; opt.textContent = `${root.id}: ${root.path}`; el.rootsSelect.appendChild(opt); } if (state.roots.length > 0) { state.currentSubpath = ""; el.subpathInput.value = ""; await loadFolders(); } out("Roots loaded", data); } async function doSearch() { const query = (el.searchInput.value || "").trim(); if (!query) return; const data = await api(`/api/tvdb/search?q=${encodeURIComponent(query)}`); el.searchResults.innerHTML = ""; (data.items || []).forEach((item) => { const li = document.createElement("li"); const left = document.createElement("span"); left.textContent = item.display_name || item.name || "(no name)"; const right = document.createElement("div"); const btn = makeActionButton("Select", async () => { state.selectedSeries = item; el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`; await loadEpisodes(); }); right.appendChild(btn); li.appendChild(left); li.appendChild(right); el.searchResults.appendChild(li); }); out("Search result", data); } async function loadEpisodes() { if (!state.selectedSeries || !state.selectedSeries.id) { throw new Error("Select a series first"); } const data = await api(`/api/tvdb/series/${encodeURIComponent(state.selectedSeries.id)}/episodes?order_type=aired`); state.episodes = data.items || []; el.episodesList.innerHTML = ""; state.episodes.forEach((episode) => { const li = document.createElement("li"); const left = document.createElement("span"); left.textContent = episode.label || `${episode.season_number}x${episode.episode_number} ${episode.title}`; const right = document.createElement("div"); const btn = makeActionButton("Add", async () => { const payload = { id: episode.id, series: state.selectedSeries.name, year: state.selectedSeries.year, season_number: episode.season_number, episode_number: episode.episode_number, title: episode.title, aired: episode.aired, label: episode.label, }; const res = await api(q("/api/session/selected-episodes"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [payload] }), }); await loadSelectedEpisodes(); out("Episode added", res); }); right.appendChild(btn); li.appendChild(left); li.appendChild(right); el.episodesList.appendChild(li); }); out("Episodes loaded", data); } async function loadSelectedEpisodes() { const data = await api(q("/api/session/selected-episodes")); el.selectedEpisodesList.innerHTML = ""; (data.items || []).forEach((item, idx) => { const li = document.createElement("li"); const left = document.createElement("span"); left.textContent = item.episode.label || item.episode.title || `Episode #${idx + 1}`; const right = document.createElement("div"); right.appendChild(makeActionButton("Up", async () => { if (idx === 0) return; await api(q("/api/session/selected-episodes/reorder"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from_index: idx, to_index: idx - 1 }), }); await loadSelectedEpisodes(); }, true)); right.appendChild(makeActionButton("Down", async () => { if (idx >= data.items.length - 1) return; await api(q("/api/session/selected-episodes/reorder"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from_index: idx, to_index: idx + 1 }), }); await loadSelectedEpisodes(); }, true)); right.appendChild(makeActionButton("Remove", async () => { await api(q(`/api/session/selected-episodes/${item.selection_id}`), { method: "DELETE" }); await loadSelectedEpisodes(); }, true)); li.appendChild(left); li.appendChild(right); el.selectedEpisodesList.appendChild(li); }); return data; } async function loadFolders() { const rootId = el.rootsSelect.value; if (!rootId) throw new Error("No root selected"); const subpath = (el.subpathInput.value || "").trim(); state.currentSubpath = subpath; const data = await api( `/api/files/folders?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(subpath)}&limit=500` ); state.folders = data.items || []; el.foldersSelect.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = state.folders.length ? "Choose folder..." : "(No folders)"; el.foldersSelect.appendChild(placeholder); state.folders.forEach((folder) => { const opt = document.createElement("option"); opt.value = folder.subpath; opt.textContent = folder.subpath || folder.name; el.foldersSelect.appendChild(opt); }); out("Folders loaded", data); } async function discoverFiles() { const rootId = el.rootsSelect.value; if (!rootId) throw new Error("No root selected"); const selectedFolder = (el.foldersSelect.value || "").trim(); if (!selectedFolder) throw new Error("Choose a folder first"); el.subpathInput.value = selectedFolder; state.currentSubpath = selectedFolder; const subpath = encodeURIComponent(selectedFolder); const recursive = el.recursiveInput.checked ? "true" : "false"; const data = await api(`/api/files/discover?root_id=${encodeURIComponent(rootId)}&subpath=${subpath}&recursive=${recursive}&limit=200`); state.discoveredFiles = data.items || []; el.discoveredFilesList.innerHTML = ""; state.discoveredFiles.forEach((file) => { const li = document.createElement("li"); const left = document.createElement("span"); left.textContent = file.relative_path || file.name; const right = document.createElement("div"); right.appendChild(makeActionButton("Add", async () => { const payload = { path: file.path, name: file.name }; const res = await api(q("/api/session/selected-files"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [payload] }), }); await loadSelectedFiles(); out("File added", res); })); li.appendChild(left); li.appendChild(right); el.discoveredFilesList.appendChild(li); }); out("Discover result", data); } async function loadSelectedFiles() { const data = await api(q("/api/session/selected-files")); el.selectedFilesList.innerHTML = ""; (data.items || []).forEach((item, idx) => { const li = document.createElement("li"); const left = document.createElement("span"); left.textContent = item.file.path || item.file.name || `File #${idx + 1}`; const right = document.createElement("div"); right.appendChild(makeActionButton("Up", async () => { if (idx === 0) return; await api(q("/api/session/selected-files/reorder"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from_index: idx, to_index: idx - 1 }), }); await loadSelectedFiles(); }, true)); right.appendChild(makeActionButton("Down", async () => { if (idx >= data.items.length - 1) return; await api(q("/api/session/selected-files/reorder"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from_index: idx, to_index: idx + 1 }), }); await loadSelectedFiles(); }, true)); right.appendChild(makeActionButton("Remove", async () => { await api(q(`/api/session/selected-files/${item.selection_id}`), { method: "DELETE" }); await loadSelectedFiles(); }, true)); li.appendChild(left); li.appendChild(right); el.selectedFilesList.appendChild(li); }); return data; } async function callPreview(path, label) { const data = await api(q(path)); out(label, data); } async function executeRename() { const data = await api(q("/api/session/rename-execute") + "&confirm=true", { method: "POST", }); if (data.executed) { out("Rename execute: success", data); // Keep UI in sync with renamed destinations using existing selected-files endpoints. const renamedFiles = (data.items || []) .filter((item) => item.status === "renamed") .map((item) => ({ path: item.destination_path, name: item.proposed_filename, })); await api(q("/api/session/selected-files"), { method: "DELETE" }); if (renamedFiles.length > 0) { await api(q("/api/session/selected-files"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: renamedFiles }), }); } await loadSelectedFiles(); return; } out("Rename execute: preflight failed", data); } async function withHandler(fn, btn) { try { setLoading(btn, true); await fn(); } catch (err) { out("Error", { detail: err.message || String(err) }); } finally { setLoading(btn, false); } } function bindEvents() { el.searchBtn.addEventListener("click", () => withHandler(doSearch, el.searchBtn)); el.refreshEpisodesBtn.addEventListener("click", () => withHandler(loadEpisodes, el.refreshEpisodesBtn)); el.refreshSelectedEpisodesBtn.addEventListener("click", () => withHandler(loadSelectedEpisodes, el.refreshSelectedEpisodesBtn)); el.clearSelectedEpisodesBtn.addEventListener("click", () => withHandler(async () => { const res = await api(q("/api/session/selected-episodes"), { method: "DELETE" }); await loadSelectedEpisodes(); out("Selected episodes cleared", res); }, el.clearSelectedEpisodesBtn) ); el.refreshRootsBtn.addEventListener("click", () => withHandler(loadRoots, el.refreshRootsBtn)); el.rootsSelect.addEventListener("change", () => withHandler(async () => { state.currentSubpath = ""; el.subpathInput.value = ""; await loadFolders(); el.discoveredFilesList.innerHTML = ""; }, el.rootsSelect)); el.loadFoldersBtn.addEventListener("click", () => withHandler(loadFolders, el.loadFoldersBtn)); el.loadFilesBtn.addEventListener("click", () => withHandler(discoverFiles, el.loadFilesBtn)); el.foldersSelect.addEventListener("change", () => { const chosen = (el.foldersSelect.value || "").trim(); if (chosen) { el.subpathInput.value = chosen; } }); el.refreshSelectedFilesBtn.addEventListener("click", () => withHandler(loadSelectedFiles, el.refreshSelectedFilesBtn)); el.clearSelectedFilesBtn.addEventListener("click", () => withHandler(async () => { const res = await api(q("/api/session/selected-files"), { method: "DELETE" }); await loadSelectedFiles(); out("Selected files cleared", res); }, el.clearSelectedFilesBtn) ); el.mappingPreviewBtn.addEventListener("click", () => withHandler(() => callPreview("/api/session/mapping-preview", "Mapping preview"), el.mappingPreviewBtn)); el.filenamePreviewBtn.addEventListener("click", () => withHandler(() => callPreview("/api/session/filename-preview", "Filename preview"), el.filenamePreviewBtn)); el.renameExecuteBtn.addEventListener("click", () => withHandler(executeRename, el.renameExecuteBtn)); } async function init() { el.sessionMeta.textContent = `session_id: ${state.sessionId}`; el.recursiveInput.checked = true; bindEvents(); await loadRoots(); await loadSelectedEpisodes(); await loadSelectedFiles(); } init().catch((err) => out("Init error", { detail: err.message || String(err) })); })();