d0a2def788
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>
329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
// ---------- 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))}`;
|
||
}
|
||
});
|
||
|