Files
ext-rclone-jav/options-rules-editors.js
T
admin d0a2def788 Step 6c: extract Diagnostics + Profiles + Rules Editors from options.js
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 <noreply@anthropic.com>
2026-05-23 11:17:55 +02:00

329 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---------- 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 = `
<td><input type="text" class="host" placeholder="clearjav.com"></td>
<td><input type="text" class="selector" placeholder=".some-class"></td>
<td><button class="del" type="button">×</button></td>`;
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 = `
<td><input type="text" class="re" placeholder="\\b1pondo-?(\\d{4,})-?(\\d{2,})\\b"></td>
<td><input type="text" class="fmt" placeholder="1pondo-$1-$2"></td>
<td><button class="del" type="button">×</button></td>`;
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 = `<span class="warn">Enter a detector regex.</span> Capture group 1 should be the part token.`;
return;
}
let re;
try {
re = partDetectorRegex(pattern);
} catch (err) {
feedback.innerHTML = `<span class="fail">Invalid preview regex:</span> ${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 = `<span class="warn">No representative sample matched.</span> 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
? `<div class="warn">These representative matches are already covered by built-in detector${new Set(covered.map((entry) => entry.detector.pattern)).size === 1 ? "" : "s"}.</div>`
: "";
feedback.innerHTML = [
`<span class="${missingCapture ? "warn" : "ok"}">${missingCapture ? "Matched, but capture group 1 was missing for a sample." : `Matches ${matches.length} representative filename shape${matches.length === 1 ? "" : "s"}.`}</span>`,
coveredNote,
...matches.slice(0, 4).map((item) => `<div class="part-detector-match">${escapeHtml(item.filename)} -> part ${escapeHtml(item.part)} <span style="color:#777;">(${escapeHtml(item.stage)})</span></div>`),
matches.length > 4 ? `<div>and ${escapeHtml(matches.length - 4)} more representative match(es)</div>` : "",
].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 = `
<div class="part-detector-head">
<input type="text" class="part-detector-pattern" placeholder="_PART(\\d+)$"${builtin ? " readonly" : ""}>
${builtin ? `<span class="part-detector-kind">Built in</span>` : `<button type="button" title="Remove detector">x</button>`}
</div>
${note ? `<div class="muted" style="margin-top:5px;">${escapeHtml(note)}</div>` : ""}
<div class="part-detector-feedback"></div>
`;
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 = `<span style="color:#faa;">error:</span> ${escapeHtml(r?.error || "no response")}`;
return;
}
const e = r.extracted || {};
out.innerHTML = [
`<div><span style="color:#777;">ID:</span> <span style="color:${e.id ? "#afa" : "#faa"};">${escapeHtml(e.id || "none")}</span></div>`,
`<div><span style="color:#777;">Rule:</span> ${escapeHtml(e.source || "none")}</div>`,
e.pattern ? `<div><span style="color:#777;">Pattern:</span> ${escapeHtml(e.pattern)}</div>` : "",
e.replacement ? `<div><span style="color:#777;">Replacement:</span> ${escapeHtml(e.replacement)}</div>` : "",
e.raw ? `<div><span style="color:#777;">Raw:</span> ${escapeHtml(e.raw)}</div>` : "",
].filter(Boolean).join("");
} catch (err) {
out.innerHTML = `<span style="color:#faa;">error:</span> ${escapeHtml(err.message || String(err))}`;
}
});