From d0a2def788e6bea126f18612c41fe619460ba3c4 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 23 May 2026 11:17:55 +0200 Subject: [PATCH] Step 6c: extract Diagnostics + Profiles + Rules Editors from options.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final options.js split. Three new files: options-diagnostics.js 245 lines options-profiles.js 265 lines options-rules-editors.js 328 lines (adapters + ID normalizers + custom part detectors) options.js: 1852 → 1014 lines (838 extracted, ~45% reduction). Script-tag order in options.html now (load order matters for top-level let bindings shared across files, e.g. _configuredScanRoots): options-cache.js options-dupe-review.js options-library-issues.js options-diagnostics.js options-profiles.js options-rules-editors.js options.js (entry: IIFE bottom, escapeHtml, overlay previews, element picker, paths) The picker, overlay-preview, and no-match overlay code stays in options.js — those are tightly intertwined with multiple settings panes and not worth further splitting today. node --check passes on each file individually and on the concatenated load-order stream. Line count of concat (3144) matches the pre-split sum exactly. Co-Authored-By: Claude Opus 4.7 --- options-diagnostics.js | 245 ++++++++++++ options-profiles.js | 265 +++++++++++++ options-rules-editors.js | 328 +++++++++++++++ options.html | 3 + options.js | 838 --------------------------------------- 5 files changed, 841 insertions(+), 838 deletions(-) create mode 100644 options-diagnostics.js create mode 100644 options-profiles.js create mode 100644 options-rules-editors.js diff --git a/options-diagnostics.js b/options-diagnostics.js new file mode 100644 index 0000000..8d523b5 --- /dev/null +++ b/options-diagnostics.js @@ -0,0 +1,245 @@ +// ---------- diagnostics ---------- + +// Extension ID display + copy button (added when Transfer Assistant was deleted). +// Diagnostics is the canonical home for "what's my extension ID?" info now. +(() => { + const idEl = document.getElementById("diag-extension-id"); + const copyBtn = document.getElementById("diag-copy-extension-id"); + if (idEl) idEl.textContent = chrome.runtime.id; + if (copyBtn) { + copyBtn.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(chrome.runtime.id); + copyBtn.textContent = "Copied"; + setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200); + } catch (_) { + copyBtn.textContent = "Copy failed"; + setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200); + } + }); + } +})(); + +document.getElementById("run-diag").addEventListener("click", (event) => + keepActionViewport(event.currentTarget, runDiagnostics) +); +document.getElementById("host-status-run").addEventListener("click", (event) => + keepActionViewport(event.currentTarget, runHostStatus) +); +document.getElementById("host-repair-run").addEventListener("click", (event) => + keepActionViewport(event.currentTarget, runHostRepair) +); +document.getElementById("host-verify-run").addEventListener("click", (event) => + keepActionViewport(event.currentTarget, runHostStatus) +); +document.getElementById("run-all-diag").addEventListener("click", (event) => + keepActionViewport(event.currentTarget, async () => { + clearNativeRepairCard(); + const runtime = await runDiagnostics(); + if (runtime && runtime.nativeBlocked) { + renderBlockedByNativeIssue(document.getElementById("host-status-results"), "Host registration"); + return; + } + await runHostStatus(); + }) +); + +function renderDiagRows(out, checks, emptyLabel) { + out.innerHTML = ""; + if (!checks || checks.length === 0) { + out.innerHTML = `
!${escapeHtml(emptyLabel)}no checks returned
`; + return; + } + const counts = checks.reduce((acc, c) => { + const status = c.status || "warn"; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {}); + const summary = document.createElement("div"); + summary.className = "diag-row " + ((counts.fail || 0) ? "fail" : (counts.warn || 0) ? "warn" : "ok"); + summary.innerHTML = `#summary${checks.length} checks · ok ${counts.ok || 0} · info ${counts.info || 0} · warn ${counts.warn || 0} · fail ${counts.fail || 0}`; + out.appendChild(summary); + for (const c of checks) { + const row = document.createElement("div"); + row.className = "diag-row " + (c.status || "warn"); + const status = c.status || "warn"; + const icon = status === "ok" ? "✓" : status === "info" ? "i" : status === "warn" ? "!" : "✗"; + row.innerHTML = `${icon}${escapeHtml(c.name)}${formatDiagDetail(c.detail || "")}`; + out.appendChild(row); + } +} + +function formatDiagDetail(detail) { + const text = String(detail || ""); + if (!text) return ""; + const shouldCollapse = text.length > 120 || text.includes("\n") || (text.match(/[;|]/g) || []).length > 2; + if (!shouldCollapse) return escapeHtml(text); + const first = text.split(/\r?\n/)[0].slice(0, 110); + return `
${escapeHtml(first)}${text.length > first.length ? "…" : ""}
${escapeHtml(text)}
`; +} + +async function runDiagnostics() { + const out = document.getElementById("diag-results"); + clearNativeRepairCard(); + out.innerHTML = '
running…waiting for native host
'; + try { + const r = await chrome.runtime.sendMessage({ type: "diagnostics" }); + if (!r || !r.ok) { + await renderNativeMessagingFailure(r); + renderBlockedByNativeIssue(out, "Runtime diagnostics"); + return { nativeBlocked: true }; + } + renderDiagRows(out, r.checks || [], "runtime"); + return { ok: true }; + } catch (err) { + out.innerHTML = `
runtime${escapeHtml(err.message || String(err))}
`; + return { ok: false }; + } +} + +async function runHostStatus() { + const out = document.getElementById("host-status-results"); + clearNativeRepairCard(); + out.innerHTML = '
checking…reading manifest and registry state
'; + try { + const r = await chrome.runtime.sendMessage({ type: "host-status" }); + if (!r || !r.ok) { + await renderNativeMessagingFailure(r); + renderBlockedByNativeIssue(out, "Native host checks"); + return { nativeBlocked: true }; + } + renderDiagRows(out, r.checks || [], "host status"); + return { ok: true }; + } catch (err) { + out.innerHTML = `
host status${escapeHtml(err.message || String(err))}
`; + return { ok: false }; + } +} + +async function runHostRepair() { + const out = document.getElementById("host-status-results"); + clearNativeRepairCard(); + out.innerHTML = '
repairing…updating reachable native host manifest and user registration
'; + try { + const r = await chrome.runtime.sendMessage({ type: "repair-host" }); + if (!r || !r.ok) { + if (r?.error_kind) { + await renderNativeMessagingFailure(r); + renderBlockedByNativeIssue(out, "Registration repair"); + } else { + out.innerHTML = `
Registration repair${escapeHtml(r?.error || "repair failed")}
`; + } + return { ok: false }; + } + const checks = r.verification?.checks || []; + renderDiagRows(out, checks, "repair verification"); + renderCompletedNativeRepair(r); + return { ok: true }; + } catch (err) { + out.innerHTML = `
Registration repair${escapeHtml(err.message || String(err))}
`; + return { ok: false }; + } +} + +function clearNativeRepairCard() { + const card = document.getElementById("native-repair-card"); + const out = document.getElementById("native-repair-results"); + const title = document.getElementById("native-repair-title"); + if (card) card.style.display = "none"; + if (out) out.innerHTML = ""; + if (title) title.textContent = "Native host setup"; +} + +function renderCompletedNativeRepair(response) { + const card = document.getElementById("native-repair-card"); + const out = document.getElementById("native-repair-results"); + if (!card || !out) return; + card.style.display = ""; + const title = document.getElementById("native-repair-title"); + if (title) title.textContent = "Registration repair completed"; + const regs = (response.registrations || []).filter((x) => x.status === "ok").length; + out.innerHTML = ` +
Repair applied${escapeHtml(response.message || "native host registration repaired")}
+
iManifest${escapeHtml(response.manifest_path || "")}
+
iUser registry${escapeHtml(`${regs} HKCU registration entr${regs === 1 ? "y" : "ies"} updated`)}
+
!Restart requiredFully close Brave, reopen it, reload the extension, then click Verify Registration. If Brave still blocks the host, run the registration steps shown by Diagnostics.
+ `; +} + +function renderBlockedByNativeIssue(out, title) { + out.innerHTML = `
i${escapeHtml(title)}Blocked until this PC registers the native host for the current extension ID. Use the setup card above.
`; +} + +async function getPackagedHostPaths() { + try { + const resp = await fetch(chrome.runtime.getURL("host/com.rcjav.host.json")); + if (!resp.ok) return {}; + const manifest = await resp.json(); + const bat = manifest.path || ""; + const hostDir = bat.replace(/[\\/][^\\/]+$/, ""); + return { + hostBat: bat, + hostDir, + registerBat: hostDir ? hostDir + "\\register-host.bat" : "", + installPs1: hostDir ? hostDir + "\\install-host.ps1" : "", + }; + } catch { + return {}; + } +} + +async function renderNativeMessagingFailure(response) { + const card = document.getElementById("native-repair-card"); + const out = document.getElementById("native-repair-results"); + if (!card || !out) return; + card.style.display = ""; + const title = document.getElementById("native-repair-title"); + if (title) title.textContent = "Register host on this PC"; + const error = response?.error || "no response"; + const kind = response?.error_kind || (/forbidden/i.test(error) ? "forbidden" : "unknown"); + const extensionId = response?.extension_id || chrome.runtime.id; + const paths = await getPackagedHostPaths(); + const installCommand = paths.installPs1 + ? `pwsh -ExecutionPolicy Bypass -File "${paths.installPs1}" -ExtensionId ${extensionId}` + : `pwsh -ExecutionPolicy Bypass -File ".\\host\\install-host.ps1" -ExtensionId ${extensionId}`; + const registerCommand = paths.registerBat ? `"${paths.registerBat}"` : ".\\host\\register-host.bat"; + let cause = "This extension cannot launch the native messaging host yet."; + let fix = "Register the host for this extension ID, fully restart Brave, then verify registration."; + if (kind === "forbidden") { + cause = "Brave found the native host, but this extension ID is not allowed to launch it on this PC."; + fix = "This usually happens after loading the extension on another PC or under a different extension ID."; + } else if (kind === "not_found") { + cause = "Brave could not find a registered native messaging host for com.rcjav.host on this PC."; + fix = "Run the registration script from the extension host folder."; + } else if (kind === "disconnected") { + cause = "The native host started and then disconnected or crashed."; + fix = "After registration is fixed, run Runtime diagnostics again to check Python, rc-jav, and rclone."; + } else if (kind === "timeout") { + cause = "The native host did not respond before the timeout."; + fix = "Restart Brave and check whether a scan or rclone command is stuck."; + } + out.innerHTML = ` +
!Setup requiredNative host registration must be fixed before cache, runtime, and host checks can run.
+
!Likely cause${escapeHtml(cause)}
+
iHost message${escapeHtml(error)}
+
Fix on this PC${escapeHtml(fix)}
+
iExtension ID${escapeHtml(extensionId)}
+
1Run register-host +
${escapeHtml(registerCommand)}
${escapeHtml(`Run ${registerCommand}\nWhen it asks for the extension ID, enter:\n${extensionId}\n\nPowerShell alternative:\n${installCommand}`)}
+ +
+
2Restart BraveClose every Brave window/process, reopen Brave, then reload the extension.
+
3Verify
+ `; + for (const btn of out.querySelectorAll("button[data-copy]")) { + btn.addEventListener("click", async () => { + await navigator.clipboard.writeText(btn.dataset.copy || ""); + btn.textContent = "Copied"; + setTimeout(() => { btn.textContent = btn.dataset.copyLabel || "Copy"; }, 1200); + }); + } + for (const btn of out.querySelectorAll("button[data-verify-registration]")) { + btn.addEventListener("click", runHostStatus); + } +} + diff --git a/options-profiles.js b/options-profiles.js new file mode 100644 index 0000000..f11b65a --- /dev/null +++ b/options-profiles.js @@ -0,0 +1,265 @@ +// ---------- profiles ---------- + +let _knownRemotes = []; // ["cq:", "gdrive:", ...] from rclone listremotes +let _cfgDefaults = { source: [], target: [] }; +let _remotesLoaded = false; + +async function fetchRemotes() { + const status = document.getElementById("profiles-status"); + if (_remotesLoaded) return; + _remotesLoaded = true; + if (status) status.textContent = "loading remotes..."; + try { + const r = await chrome.runtime.sendMessage({ type: "list-remotes" }); + if (r && r.ok) { + _knownRemotes = r.remotes || []; + _cfgDefaults = { source: r.default_source || [], target: r.default_target || [] }; + if (status) status.textContent = `${_knownRemotes.length} remote(s) loaded`; + // Re-render to populate selects now that we have data + const profiles = readProfiles(); + renderProfiles(profiles); + updateSectionSummaries(); + } + } catch (e) { + _remotesLoaded = false; + if (status) status.textContent = "failed to load remotes"; + } +} + +document.querySelector('.side .item[data-pane="profiles"]').addEventListener("click", fetchRemotes); +document.getElementById("load-remotes").addEventListener("click", () => { + _remotesLoaded = false; + fetchRemotes(); +}); + +/** + * Build a remote picker widget. + * Shows: a + + `; + tr.querySelector(".host").value = host; + tr.querySelector(".selector").value = selector; + tr.querySelector(".del").addEventListener("click", () => tr.remove()); + tbody.appendChild(tr); +} + +function readAdapters() { + const rows = document.querySelectorAll("#adapters tbody tr"); + const out = []; + for (const tr of rows) { + const host = tr.querySelector(".host").value.trim(); + const selector = tr.querySelector(".selector").value.trim(); + if (host && selector) out.push({ host, selector }); + } + return out; +} + +document.getElementById("add-adapter").addEventListener("click", () => addAdapterRow("", "")); + +document.getElementById("validate-adapters").addEventListener("click", () => { + const status = document.getElementById("picker-status"); + const rows = [...document.querySelectorAll("#adapters tbody tr")]; + const seen = new Set(); + const issues = []; + for (const tr of rows) { + const host = tr.querySelector(".host").value.trim(); + const selector = tr.querySelector(".selector").value.trim(); + tr.style.outline = ""; + if (!host && !selector) continue; + if (!host || !selector) { + issues.push("rows need both host and selector"); + tr.style.outline = "1px solid #775"; + continue; + } + const key = host.toLowerCase(); + if (seen.has(key)) { + issues.push(`duplicate host: ${host}`); + tr.style.outline = "1px solid #775"; + } + seen.add(key); + try { document.querySelector(selector); } catch { + issues.push(`invalid CSS selector for ${host}`); + tr.style.outline = "1px solid #775"; + } + } + status.textContent = issues.length ? [...new Set(issues)].join("; ") : `${readAdapters().length} adapter row(s) look valid`; + updateSectionSummaries(); +}); + +// ---------- ID normalizers ---------- + +function renderNormalizers(list) { + const tbody = document.querySelector("#normalizers tbody"); + tbody.innerHTML = ""; + for (const n of list) addNormalizerRow(n.re || "", n.fmt || ""); + if (list.length === 0) addNormalizerRow("", ""); +} + +function addNormalizerRow(re, fmt) { + const tbody = document.querySelector("#normalizers tbody"); + const tr = document.createElement("tr"); + tr.innerHTML = ` + + + `; + tr.querySelector(".re").value = re; + tr.querySelector(".fmt").value = fmt; + tr.querySelector(".del").addEventListener("click", () => tr.remove()); + tbody.appendChild(tr); +} + +function readNormalizers() { + const rows = document.querySelectorAll("#normalizers tbody tr"); + const out = []; + for (const tr of rows) { + const re = tr.querySelector(".re").value.trim(); + const fmt = tr.querySelector(".fmt").value.trim(); + if (re && fmt) out.push({ re, fmt }); + } + return out; +} + +document.getElementById("add-normalizer").addEventListener("click", () => addNormalizerRow("", "")); + +document.getElementById("validate-normalizers").addEventListener("click", () => { + const status = document.getElementById("normalizer-status"); + const rows = [...document.querySelectorAll("#normalizers tbody tr")]; + const issues = []; + for (const tr of rows) { + tr.style.outline = ""; + const re = tr.querySelector(".re").value.trim(); + const fmt = tr.querySelector(".fmt").value.trim(); + if (!re && !fmt) continue; + if (!re || !fmt) { + issues.push("rows need both regex and replacement"); + tr.style.outline = "1px solid #775"; + continue; + } + try { new RegExp(re, "i"); } catch (err) { + issues.push(`invalid regex: ${err.message}`); + tr.style.outline = "1px solid #775"; + } + } + status.textContent = issues.length ? issues.join("; ") : `${readNormalizers().length} normalizer row(s) look valid`; + updateSectionSummaries(); +}); + +// ---------- custom part detectors ---------- + +const PART_DETECTOR_SAMPLES = [ + "KV-118 - Aiba Reika_PART1.mp4", + "KV-118 - Aiba Reika_PART2.mp4", + "KV-118 - Aiba Reika_PART3.mp4", + "KV-118_1.mp4", + "KV-118_2.mp4", + "KV-118-pt1.mp4", + "KV-118-part2.mp4", + "KV-118-cd1.mp4", + "KV-118-disc2.mp4", + "KV-118 (1).mp4", + "KV-118 (1 of 3).mp4", + "KV-118.1of3.mp4", + "KV-118-2 of 4.mp4", + "OFJE-195-1 [480p].mp4", + "OFJE-195-2 [480p].mp4", + "OFJE-195-3 [480p].mp4", + "KV-118_A.mp4", + "KV-118-B.mp4", + "KV-118A.mp4", + "KV-118 1.mp4", + "KV-118-P1.mp4", + "KV-118_P2.mp4", + "KV-118 Part 3.mp4", + "KV-118_EP1.mp4", + "KV-118 Episode 2.mp4", + "KV-118_Vol1.mp4", + "KV-118 Volume 2.mp4", + "KV-118_Scene1.mp4", + "KV-118_Side-A.mp4", +]; + +const BUILTIN_PART_DETECTORS = [ + { pattern: "[-_ ](?:pt|part|cd|disc)[-_ ]?(\\d+)$", note: "pt / part / cd / disc number" }, + { pattern: "\\s*\\((\\d+)(?:\\s*of\\s*\\d+)?\\)$", note: "parenthesized part number or X of Y" }, + { pattern: "[._ -](\\d+)\\s*of\\s*\\d+$", note: "X of Y suffix" }, + { pattern: "_(\\d{1,2})$", note: "underscore number" }, + { pattern: "-(\\d{1,2})$", note: "hyphen short part number" }, + { pattern: "[-_]([A-D])$", note: "lettered part with separator" }, + { pattern: "(?<=\\d)([A-D])$", note: "lettered part directly after ID" }, + { pattern: "\\s+(\\d{1,2})$", note: "trailing spaced number" }, +]; + +function partDetectorStem(filename) { + return filename.replace(/\.[^.]+$/, ""); +} + +function partDetectorStemStages(filename) { + const raw = partDetectorStem(filename); + const resolutionClean = raw.replace(/\s*\[[^\]]*\]\s*$/, "").trim(); + let actressClean = resolutionClean; + if (actressClean.includes(" - ")) actressClean = actressClean.slice(0, actressClean.indexOf(" - ")).trim(); + const stages = []; + for (const [label, stem] of [ + ["raw stem", raw], + ["after trailing metadata cleanup", resolutionClean], + ["after actress cleanup", actressClean], + ]) { + if (stem && !stages.some((stage) => stage.stem === stem)) stages.push({ label, stem }); + } + return stages; +} + +function partDetectorRegex(pattern) { + // Custom detectors are Python regexes, but the common detector subset is + // shared with browser RegExp. Preview the representative shapes here; rc-jav + // remains authoritative when the saved rule runs during scan/search. + return new RegExp(pattern, "i"); +} + +function builtinPartCoverage(filename) { + for (const detector of BUILTIN_PART_DETECTORS) { + try { + const re = partDetectorRegex(detector.pattern); + for (const stage of partDetectorStemStages(filename)) { + const match = stage.stem.match(re); + if (match && match[1]) return detector; + } + } catch {} + } + return null; +} + +function updatePartDetectorFeedback(row) { + const feedback = row.querySelector(".part-detector-feedback"); + const pattern = row.querySelector(".part-detector-pattern").value.trim(); + if (!pattern) { + feedback.innerHTML = `Enter a detector regex. Capture group 1 should be the part token.`; + return; + } + let re; + try { + re = partDetectorRegex(pattern); + } catch (err) { + feedback.innerHTML = `Invalid preview regex: ${escapeHtml(err.message || String(err))}`; + return; + } + const matches = []; + let missingCapture = false; + for (const filename of PART_DETECTOR_SAMPLES) { + for (const stage of partDetectorStemStages(filename)) { + const match = stage.stem.match(re); + if (!match) continue; + if (!match[1]) missingCapture = true; + matches.push({ filename, part: match[1] || "?", stage: stage.label }); + break; + } + } + if (!matches.length) { + feedback.innerHTML = `No representative sample matched. The rule may still be valid for a library-specific filename shape.`; + return; + } + const isBuiltin = row.classList.contains("builtin"); + const covered = !isBuiltin ? matches.map((item) => ({ item, detector: builtinPartCoverage(item.filename) })) : []; + const alreadyCovered = covered.length && covered.every((entry) => entry.detector); + const coveredNote = alreadyCovered + ? `
These representative matches are already covered by built-in detector${new Set(covered.map((entry) => entry.detector.pattern)).size === 1 ? "" : "s"}.
` + : ""; + feedback.innerHTML = [ + `${missingCapture ? "Matched, but capture group 1 was missing for a sample." : `Matches ${matches.length} representative filename shape${matches.length === 1 ? "" : "s"}.`}`, + coveredNote, + ...matches.slice(0, 4).map((item) => `
${escapeHtml(item.filename)} -> part ${escapeHtml(item.part)} (${escapeHtml(item.stage)})
`), + matches.length > 4 ? `
and ${escapeHtml(matches.length - 4)} more representative match(es)
` : "", + ].filter(Boolean).join(""); +} + +function addPartDetectorRow(pattern = "", { builtin = false, note = "" } = {}) { + const list = document.getElementById(builtin ? "builtin-part-detectors" : "part-detectors"); + const row = document.createElement("div"); + row.className = "part-detector-row" + (builtin ? " builtin" : ""); + row.innerHTML = ` +
+ + ${builtin ? `Built in` : ``} +
+ ${note ? `
${escapeHtml(note)}
` : ""} +
+ `; + row.querySelector(".part-detector-pattern").value = pattern; + if (!builtin) { + row.querySelector(".part-detector-pattern").addEventListener("input", () => { + updatePartDetectorFeedback(row); + updateSectionSummaries(); + }); + row.querySelector("button").addEventListener("click", () => { + row.remove(); + if (!list.children.length) addPartDetectorRow(""); + updateSectionSummaries(); + }); + } + list.appendChild(row); + updatePartDetectorFeedback(row); + return row; +} + +function renderPartDetectors(patterns) { + const builtinList = document.getElementById("builtin-part-detectors"); + const list = document.getElementById("part-detectors"); + builtinList.innerHTML = ""; + list.innerHTML = ""; + for (const detector of BUILTIN_PART_DETECTORS) addPartDetectorRow(detector.pattern, { builtin: true, note: detector.note }); + for (const pattern of patterns || []) addPartDetectorRow(pattern); + if (!list.children.length) addPartDetectorRow(""); +} + +function readPartDetectors() { + return [...document.querySelectorAll("#part-detectors .part-detector-pattern")] + .map((input) => input.value.trim()) + .filter(Boolean); +} + +document.getElementById("add-part-detector").addEventListener("click", () => { + addPartDetectorRow("").querySelector(".part-detector-pattern").focus(); +}); + +// Tester +document.getElementById("norm-test-run").addEventListener("click", async () => { + const input = document.getElementById("norm-test-in").value; + const out = document.getElementById("norm-test-out"); + out.textContent = "testing text..."; + try { + const r = await chrome.runtime.sendMessage({ + type: "test-id-text", + text: input, + normalizers: readNormalizers(), + }); + if (!r || !r.ok) { + out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`; + return; + } + const e = r.extracted || {}; + out.innerHTML = [ + `
ID: ${escapeHtml(e.id || "none")}
`, + `
Rule: ${escapeHtml(e.source || "none")}
`, + e.pattern ? `
Pattern: ${escapeHtml(e.pattern)}
` : "", + e.replacement ? `
Replacement: ${escapeHtml(e.replacement)}
` : "", + e.raw ? `
Raw: ${escapeHtml(e.raw)}
` : "", + ].filter(Boolean).join(""); + } catch (err) { + out.innerHTML = `error: ${escapeHtml(err.message || String(err))}`; + } +}); + diff --git a/options.html b/options.html index 74f3f86..482ed48 100644 --- a/options.html +++ b/options.html @@ -794,6 +794,9 @@ + + + diff --git a/options.js b/options.js index 71d44c5..a9c5f49 100644 --- a/options.js +++ b/options.js @@ -674,334 +674,6 @@ document.getElementById("search-bench-clear").addEventListener("click", () => { document.getElementById("search-bench-results").innerHTML = ""; }); -// ---------- adapters ---------- - -function renderAdapters(list) { - const tbody = document.querySelector("#adapters tbody"); - tbody.innerHTML = ""; - for (const a of list) addAdapterRow(a.host || "", a.selector || ""); - if (list.length === 0) addAdapterRow("", ""); -} - -function addAdapterRow(host, selector) { - const tbody = document.querySelector("#adapters tbody"); - const tr = document.createElement("tr"); - tr.innerHTML = ` - - - `; - tr.querySelector(".host").value = host; - tr.querySelector(".selector").value = selector; - tr.querySelector(".del").addEventListener("click", () => tr.remove()); - tbody.appendChild(tr); -} - -function readAdapters() { - const rows = document.querySelectorAll("#adapters tbody tr"); - const out = []; - for (const tr of rows) { - const host = tr.querySelector(".host").value.trim(); - const selector = tr.querySelector(".selector").value.trim(); - if (host && selector) out.push({ host, selector }); - } - return out; -} - -document.getElementById("add-adapter").addEventListener("click", () => addAdapterRow("", "")); - -document.getElementById("validate-adapters").addEventListener("click", () => { - const status = document.getElementById("picker-status"); - const rows = [...document.querySelectorAll("#adapters tbody tr")]; - const seen = new Set(); - const issues = []; - for (const tr of rows) { - const host = tr.querySelector(".host").value.trim(); - const selector = tr.querySelector(".selector").value.trim(); - tr.style.outline = ""; - if (!host && !selector) continue; - if (!host || !selector) { - issues.push("rows need both host and selector"); - tr.style.outline = "1px solid #775"; - continue; - } - const key = host.toLowerCase(); - if (seen.has(key)) { - issues.push(`duplicate host: ${host}`); - tr.style.outline = "1px solid #775"; - } - seen.add(key); - try { document.querySelector(selector); } catch { - issues.push(`invalid CSS selector for ${host}`); - tr.style.outline = "1px solid #775"; - } - } - status.textContent = issues.length ? [...new Set(issues)].join("; ") : `${readAdapters().length} adapter row(s) look valid`; - updateSectionSummaries(); -}); - -// ---------- ID normalizers ---------- - -function renderNormalizers(list) { - const tbody = document.querySelector("#normalizers tbody"); - tbody.innerHTML = ""; - for (const n of list) addNormalizerRow(n.re || "", n.fmt || ""); - if (list.length === 0) addNormalizerRow("", ""); -} - -function addNormalizerRow(re, fmt) { - const tbody = document.querySelector("#normalizers tbody"); - const tr = document.createElement("tr"); - tr.innerHTML = ` - - - `; - tr.querySelector(".re").value = re; - tr.querySelector(".fmt").value = fmt; - tr.querySelector(".del").addEventListener("click", () => tr.remove()); - tbody.appendChild(tr); -} - -function readNormalizers() { - const rows = document.querySelectorAll("#normalizers tbody tr"); - const out = []; - for (const tr of rows) { - const re = tr.querySelector(".re").value.trim(); - const fmt = tr.querySelector(".fmt").value.trim(); - if (re && fmt) out.push({ re, fmt }); - } - return out; -} - -document.getElementById("add-normalizer").addEventListener("click", () => addNormalizerRow("", "")); - -document.getElementById("validate-normalizers").addEventListener("click", () => { - const status = document.getElementById("normalizer-status"); - const rows = [...document.querySelectorAll("#normalizers tbody tr")]; - const issues = []; - for (const tr of rows) { - tr.style.outline = ""; - const re = tr.querySelector(".re").value.trim(); - const fmt = tr.querySelector(".fmt").value.trim(); - if (!re && !fmt) continue; - if (!re || !fmt) { - issues.push("rows need both regex and replacement"); - tr.style.outline = "1px solid #775"; - continue; - } - try { new RegExp(re, "i"); } catch (err) { - issues.push(`invalid regex: ${err.message}`); - tr.style.outline = "1px solid #775"; - } - } - status.textContent = issues.length ? issues.join("; ") : `${readNormalizers().length} normalizer row(s) look valid`; - updateSectionSummaries(); -}); - -// ---------- custom part detectors ---------- - -const PART_DETECTOR_SAMPLES = [ - "KV-118 - Aiba Reika_PART1.mp4", - "KV-118 - Aiba Reika_PART2.mp4", - "KV-118 - Aiba Reika_PART3.mp4", - "KV-118_1.mp4", - "KV-118_2.mp4", - "KV-118-pt1.mp4", - "KV-118-part2.mp4", - "KV-118-cd1.mp4", - "KV-118-disc2.mp4", - "KV-118 (1).mp4", - "KV-118 (1 of 3).mp4", - "KV-118.1of3.mp4", - "KV-118-2 of 4.mp4", - "OFJE-195-1 [480p].mp4", - "OFJE-195-2 [480p].mp4", - "OFJE-195-3 [480p].mp4", - "KV-118_A.mp4", - "KV-118-B.mp4", - "KV-118A.mp4", - "KV-118 1.mp4", - "KV-118-P1.mp4", - "KV-118_P2.mp4", - "KV-118 Part 3.mp4", - "KV-118_EP1.mp4", - "KV-118 Episode 2.mp4", - "KV-118_Vol1.mp4", - "KV-118 Volume 2.mp4", - "KV-118_Scene1.mp4", - "KV-118_Side-A.mp4", -]; - -const BUILTIN_PART_DETECTORS = [ - { pattern: "[-_ ](?:pt|part|cd|disc)[-_ ]?(\\d+)$", note: "pt / part / cd / disc number" }, - { pattern: "\\s*\\((\\d+)(?:\\s*of\\s*\\d+)?\\)$", note: "parenthesized part number or X of Y" }, - { pattern: "[._ -](\\d+)\\s*of\\s*\\d+$", note: "X of Y suffix" }, - { pattern: "_(\\d{1,2})$", note: "underscore number" }, - { pattern: "-(\\d{1,2})$", note: "hyphen short part number" }, - { pattern: "[-_]([A-D])$", note: "lettered part with separator" }, - { pattern: "(?<=\\d)([A-D])$", note: "lettered part directly after ID" }, - { pattern: "\\s+(\\d{1,2})$", note: "trailing spaced number" }, -]; - -function partDetectorStem(filename) { - return filename.replace(/\.[^.]+$/, ""); -} - -function partDetectorStemStages(filename) { - const raw = partDetectorStem(filename); - const resolutionClean = raw.replace(/\s*\[[^\]]*\]\s*$/, "").trim(); - let actressClean = resolutionClean; - if (actressClean.includes(" - ")) actressClean = actressClean.slice(0, actressClean.indexOf(" - ")).trim(); - const stages = []; - for (const [label, stem] of [ - ["raw stem", raw], - ["after trailing metadata cleanup", resolutionClean], - ["after actress cleanup", actressClean], - ]) { - if (stem && !stages.some((stage) => stage.stem === stem)) stages.push({ label, stem }); - } - return stages; -} - -function partDetectorRegex(pattern) { - // Custom detectors are Python regexes, but the common detector subset is - // shared with browser RegExp. Preview the representative shapes here; rc-jav - // remains authoritative when the saved rule runs during scan/search. - return new RegExp(pattern, "i"); -} - -function builtinPartCoverage(filename) { - for (const detector of BUILTIN_PART_DETECTORS) { - try { - const re = partDetectorRegex(detector.pattern); - for (const stage of partDetectorStemStages(filename)) { - const match = stage.stem.match(re); - if (match && match[1]) return detector; - } - } catch {} - } - return null; -} - -function updatePartDetectorFeedback(row) { - const feedback = row.querySelector(".part-detector-feedback"); - const pattern = row.querySelector(".part-detector-pattern").value.trim(); - if (!pattern) { - feedback.innerHTML = `Enter a detector regex. Capture group 1 should be the part token.`; - return; - } - let re; - try { - re = partDetectorRegex(pattern); - } catch (err) { - feedback.innerHTML = `Invalid preview regex: ${escapeHtml(err.message || String(err))}`; - return; - } - const matches = []; - let missingCapture = false; - for (const filename of PART_DETECTOR_SAMPLES) { - for (const stage of partDetectorStemStages(filename)) { - const match = stage.stem.match(re); - if (!match) continue; - if (!match[1]) missingCapture = true; - matches.push({ filename, part: match[1] || "?", stage: stage.label }); - break; - } - } - if (!matches.length) { - feedback.innerHTML = `No representative sample matched. The rule may still be valid for a library-specific filename shape.`; - return; - } - const isBuiltin = row.classList.contains("builtin"); - const covered = !isBuiltin ? matches.map((item) => ({ item, detector: builtinPartCoverage(item.filename) })) : []; - const alreadyCovered = covered.length && covered.every((entry) => entry.detector); - const coveredNote = alreadyCovered - ? `
These representative matches are already covered by built-in detector${new Set(covered.map((entry) => entry.detector.pattern)).size === 1 ? "" : "s"}.
` - : ""; - feedback.innerHTML = [ - `${missingCapture ? "Matched, but capture group 1 was missing for a sample." : `Matches ${matches.length} representative filename shape${matches.length === 1 ? "" : "s"}.`}`, - coveredNote, - ...matches.slice(0, 4).map((item) => `
${escapeHtml(item.filename)} -> part ${escapeHtml(item.part)} (${escapeHtml(item.stage)})
`), - matches.length > 4 ? `
and ${escapeHtml(matches.length - 4)} more representative match(es)
` : "", - ].filter(Boolean).join(""); -} - -function addPartDetectorRow(pattern = "", { builtin = false, note = "" } = {}) { - const list = document.getElementById(builtin ? "builtin-part-detectors" : "part-detectors"); - const row = document.createElement("div"); - row.className = "part-detector-row" + (builtin ? " builtin" : ""); - row.innerHTML = ` -
- - ${builtin ? `Built in` : ``} -
- ${note ? `
${escapeHtml(note)}
` : ""} -
- `; - row.querySelector(".part-detector-pattern").value = pattern; - if (!builtin) { - row.querySelector(".part-detector-pattern").addEventListener("input", () => { - updatePartDetectorFeedback(row); - updateSectionSummaries(); - }); - row.querySelector("button").addEventListener("click", () => { - row.remove(); - if (!list.children.length) addPartDetectorRow(""); - updateSectionSummaries(); - }); - } - list.appendChild(row); - updatePartDetectorFeedback(row); - return row; -} - -function renderPartDetectors(patterns) { - const builtinList = document.getElementById("builtin-part-detectors"); - const list = document.getElementById("part-detectors"); - builtinList.innerHTML = ""; - list.innerHTML = ""; - for (const detector of BUILTIN_PART_DETECTORS) addPartDetectorRow(detector.pattern, { builtin: true, note: detector.note }); - for (const pattern of patterns || []) addPartDetectorRow(pattern); - if (!list.children.length) addPartDetectorRow(""); -} - -function readPartDetectors() { - return [...document.querySelectorAll("#part-detectors .part-detector-pattern")] - .map((input) => input.value.trim()) - .filter(Boolean); -} - -document.getElementById("add-part-detector").addEventListener("click", () => { - addPartDetectorRow("").querySelector(".part-detector-pattern").focus(); -}); - -// Tester -document.getElementById("norm-test-run").addEventListener("click", async () => { - const input = document.getElementById("norm-test-in").value; - const out = document.getElementById("norm-test-out"); - out.textContent = "testing text..."; - try { - const r = await chrome.runtime.sendMessage({ - type: "test-id-text", - text: input, - normalizers: readNormalizers(), - }); - if (!r || !r.ok) { - out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`; - return; - } - const e = r.extracted || {}; - out.innerHTML = [ - `
ID: ${escapeHtml(e.id || "none")}
`, - `
Rule: ${escapeHtml(e.source || "none")}
`, - e.pattern ? `
Pattern: ${escapeHtml(e.pattern)}
` : "", - e.replacement ? `
Replacement: ${escapeHtml(e.replacement)}
` : "", - e.raw ? `
Raw: ${escapeHtml(e.raw)}
` : "", - ].filter(Boolean).join(""); - } catch (err) { - out.innerHTML = `error: ${escapeHtml(err.message || String(err))}`; - } -}); - // ---------- element picker ---------- // Track the active picker poll so re-clicking "Pick Element" cancels the prior @@ -1300,520 +972,10 @@ document.getElementById("overlay-reset").addEventListener("click", () => { }); -// ---------- diagnostics ---------- - -// Extension ID display + copy button (added when Transfer Assistant was deleted). -// Diagnostics is the canonical home for "what's my extension ID?" info now. -(() => { - const idEl = document.getElementById("diag-extension-id"); - const copyBtn = document.getElementById("diag-copy-extension-id"); - if (idEl) idEl.textContent = chrome.runtime.id; - if (copyBtn) { - copyBtn.addEventListener("click", async () => { - try { - await navigator.clipboard.writeText(chrome.runtime.id); - copyBtn.textContent = "Copied"; - setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200); - } catch (_) { - copyBtn.textContent = "Copy failed"; - setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200); - } - }); - } -})(); - -document.getElementById("run-diag").addEventListener("click", (event) => - keepActionViewport(event.currentTarget, runDiagnostics) -); -document.getElementById("host-status-run").addEventListener("click", (event) => - keepActionViewport(event.currentTarget, runHostStatus) -); -document.getElementById("host-repair-run").addEventListener("click", (event) => - keepActionViewport(event.currentTarget, runHostRepair) -); -document.getElementById("host-verify-run").addEventListener("click", (event) => - keepActionViewport(event.currentTarget, runHostStatus) -); -document.getElementById("run-all-diag").addEventListener("click", (event) => - keepActionViewport(event.currentTarget, async () => { - clearNativeRepairCard(); - const runtime = await runDiagnostics(); - if (runtime && runtime.nativeBlocked) { - renderBlockedByNativeIssue(document.getElementById("host-status-results"), "Host registration"); - return; - } - await runHostStatus(); - }) -); - -function renderDiagRows(out, checks, emptyLabel) { - out.innerHTML = ""; - if (!checks || checks.length === 0) { - out.innerHTML = `
!${escapeHtml(emptyLabel)}no checks returned
`; - return; - } - const counts = checks.reduce((acc, c) => { - const status = c.status || "warn"; - acc[status] = (acc[status] || 0) + 1; - return acc; - }, {}); - const summary = document.createElement("div"); - summary.className = "diag-row " + ((counts.fail || 0) ? "fail" : (counts.warn || 0) ? "warn" : "ok"); - summary.innerHTML = `#summary${checks.length} checks · ok ${counts.ok || 0} · info ${counts.info || 0} · warn ${counts.warn || 0} · fail ${counts.fail || 0}`; - out.appendChild(summary); - for (const c of checks) { - const row = document.createElement("div"); - row.className = "diag-row " + (c.status || "warn"); - const status = c.status || "warn"; - const icon = status === "ok" ? "✓" : status === "info" ? "i" : status === "warn" ? "!" : "✗"; - row.innerHTML = `${icon}${escapeHtml(c.name)}${formatDiagDetail(c.detail || "")}`; - out.appendChild(row); - } -} - -function formatDiagDetail(detail) { - const text = String(detail || ""); - if (!text) return ""; - const shouldCollapse = text.length > 120 || text.includes("\n") || (text.match(/[;|]/g) || []).length > 2; - if (!shouldCollapse) return escapeHtml(text); - const first = text.split(/\r?\n/)[0].slice(0, 110); - return `
${escapeHtml(first)}${text.length > first.length ? "…" : ""}
${escapeHtml(text)}
`; -} - -async function runDiagnostics() { - const out = document.getElementById("diag-results"); - clearNativeRepairCard(); - out.innerHTML = '
running…waiting for native host
'; - try { - const r = await chrome.runtime.sendMessage({ type: "diagnostics" }); - if (!r || !r.ok) { - await renderNativeMessagingFailure(r); - renderBlockedByNativeIssue(out, "Runtime diagnostics"); - return { nativeBlocked: true }; - } - renderDiagRows(out, r.checks || [], "runtime"); - return { ok: true }; - } catch (err) { - out.innerHTML = `
runtime${escapeHtml(err.message || String(err))}
`; - return { ok: false }; - } -} - -async function runHostStatus() { - const out = document.getElementById("host-status-results"); - clearNativeRepairCard(); - out.innerHTML = '
checking…reading manifest and registry state
'; - try { - const r = await chrome.runtime.sendMessage({ type: "host-status" }); - if (!r || !r.ok) { - await renderNativeMessagingFailure(r); - renderBlockedByNativeIssue(out, "Native host checks"); - return { nativeBlocked: true }; - } - renderDiagRows(out, r.checks || [], "host status"); - return { ok: true }; - } catch (err) { - out.innerHTML = `
host status${escapeHtml(err.message || String(err))}
`; - return { ok: false }; - } -} - -async function runHostRepair() { - const out = document.getElementById("host-status-results"); - clearNativeRepairCard(); - out.innerHTML = '
repairing…updating reachable native host manifest and user registration
'; - try { - const r = await chrome.runtime.sendMessage({ type: "repair-host" }); - if (!r || !r.ok) { - if (r?.error_kind) { - await renderNativeMessagingFailure(r); - renderBlockedByNativeIssue(out, "Registration repair"); - } else { - out.innerHTML = `
Registration repair${escapeHtml(r?.error || "repair failed")}
`; - } - return { ok: false }; - } - const checks = r.verification?.checks || []; - renderDiagRows(out, checks, "repair verification"); - renderCompletedNativeRepair(r); - return { ok: true }; - } catch (err) { - out.innerHTML = `
Registration repair${escapeHtml(err.message || String(err))}
`; - return { ok: false }; - } -} - -function clearNativeRepairCard() { - const card = document.getElementById("native-repair-card"); - const out = document.getElementById("native-repair-results"); - const title = document.getElementById("native-repair-title"); - if (card) card.style.display = "none"; - if (out) out.innerHTML = ""; - if (title) title.textContent = "Native host setup"; -} - -function renderCompletedNativeRepair(response) { - const card = document.getElementById("native-repair-card"); - const out = document.getElementById("native-repair-results"); - if (!card || !out) return; - card.style.display = ""; - const title = document.getElementById("native-repair-title"); - if (title) title.textContent = "Registration repair completed"; - const regs = (response.registrations || []).filter((x) => x.status === "ok").length; - out.innerHTML = ` -
Repair applied${escapeHtml(response.message || "native host registration repaired")}
-
iManifest${escapeHtml(response.manifest_path || "")}
-
iUser registry${escapeHtml(`${regs} HKCU registration entr${regs === 1 ? "y" : "ies"} updated`)}
-
!Restart requiredFully close Brave, reopen it, reload the extension, then click Verify Registration. If Brave still blocks the host, run the registration steps shown by Diagnostics.
- `; -} - -function renderBlockedByNativeIssue(out, title) { - out.innerHTML = `
i${escapeHtml(title)}Blocked until this PC registers the native host for the current extension ID. Use the setup card above.
`; -} - -async function getPackagedHostPaths() { - try { - const resp = await fetch(chrome.runtime.getURL("host/com.rcjav.host.json")); - if (!resp.ok) return {}; - const manifest = await resp.json(); - const bat = manifest.path || ""; - const hostDir = bat.replace(/[\\/][^\\/]+$/, ""); - return { - hostBat: bat, - hostDir, - registerBat: hostDir ? hostDir + "\\register-host.bat" : "", - installPs1: hostDir ? hostDir + "\\install-host.ps1" : "", - }; - } catch { - return {}; - } -} - -async function renderNativeMessagingFailure(response) { - const card = document.getElementById("native-repair-card"); - const out = document.getElementById("native-repair-results"); - if (!card || !out) return; - card.style.display = ""; - const title = document.getElementById("native-repair-title"); - if (title) title.textContent = "Register host on this PC"; - const error = response?.error || "no response"; - const kind = response?.error_kind || (/forbidden/i.test(error) ? "forbidden" : "unknown"); - const extensionId = response?.extension_id || chrome.runtime.id; - const paths = await getPackagedHostPaths(); - const installCommand = paths.installPs1 - ? `pwsh -ExecutionPolicy Bypass -File "${paths.installPs1}" -ExtensionId ${extensionId}` - : `pwsh -ExecutionPolicy Bypass -File ".\\host\\install-host.ps1" -ExtensionId ${extensionId}`; - const registerCommand = paths.registerBat ? `"${paths.registerBat}"` : ".\\host\\register-host.bat"; - let cause = "This extension cannot launch the native messaging host yet."; - let fix = "Register the host for this extension ID, fully restart Brave, then verify registration."; - if (kind === "forbidden") { - cause = "Brave found the native host, but this extension ID is not allowed to launch it on this PC."; - fix = "This usually happens after loading the extension on another PC or under a different extension ID."; - } else if (kind === "not_found") { - cause = "Brave could not find a registered native messaging host for com.rcjav.host on this PC."; - fix = "Run the registration script from the extension host folder."; - } else if (kind === "disconnected") { - cause = "The native host started and then disconnected or crashed."; - fix = "After registration is fixed, run Runtime diagnostics again to check Python, rc-jav, and rclone."; - } else if (kind === "timeout") { - cause = "The native host did not respond before the timeout."; - fix = "Restart Brave and check whether a scan or rclone command is stuck."; - } - out.innerHTML = ` -
!Setup requiredNative host registration must be fixed before cache, runtime, and host checks can run.
-
!Likely cause${escapeHtml(cause)}
-
iHost message${escapeHtml(error)}
-
Fix on this PC${escapeHtml(fix)}
-
iExtension ID${escapeHtml(extensionId)}
-
1Run register-host -
${escapeHtml(registerCommand)}
${escapeHtml(`Run ${registerCommand}\nWhen it asks for the extension ID, enter:\n${extensionId}\n\nPowerShell alternative:\n${installCommand}`)}
- -
-
2Restart BraveClose every Brave window/process, reopen Brave, then reload the extension.
-
3Verify
- `; - for (const btn of out.querySelectorAll("button[data-copy]")) { - btn.addEventListener("click", async () => { - await navigator.clipboard.writeText(btn.dataset.copy || ""); - btn.textContent = "Copied"; - setTimeout(() => { btn.textContent = btn.dataset.copyLabel || "Copy"; }, 1200); - }); - } - for (const btn of out.querySelectorAll("button[data-verify-registration]")) { - btn.addEventListener("click", runHostStatus); - } -} - function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } -// ---------- profiles ---------- - -let _knownRemotes = []; // ["cq:", "gdrive:", ...] from rclone listremotes -let _cfgDefaults = { source: [], target: [] }; -let _remotesLoaded = false; - -async function fetchRemotes() { - const status = document.getElementById("profiles-status"); - if (_remotesLoaded) return; - _remotesLoaded = true; - if (status) status.textContent = "loading remotes..."; - try { - const r = await chrome.runtime.sendMessage({ type: "list-remotes" }); - if (r && r.ok) { - _knownRemotes = r.remotes || []; - _cfgDefaults = { source: r.default_source || [], target: r.default_target || [] }; - if (status) status.textContent = `${_knownRemotes.length} remote(s) loaded`; - // Re-render to populate selects now that we have data - const profiles = readProfiles(); - renderProfiles(profiles); - updateSectionSummaries(); - } - } catch (e) { - _remotesLoaded = false; - if (status) status.textContent = "failed to load remotes"; - } -} - -document.querySelector('.side .item[data-pane="profiles"]').addEventListener("click", fetchRemotes); -document.getElementById("load-remotes").addEventListener("click", () => { - _remotesLoaded = false; - fetchRemotes(); -}); - -/** - * Build a remote picker widget. - * Shows: a