// ---------- 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))}`; } });