过滤部分 HLS 动漫源的插播切片广告,可切换为只播放识别到的广告
// ==UserScript==
// @name Adbandon
// @namespace https://greasyfork.org/users/1440044
// @version 0.1.5
// @description 过滤部分 HLS 动漫源的插播切片广告,可切换为只播放识别到的广告
// @license MIT
// @match *://enlienli.link/*
// @match *://*.enlienli.link/*
// @match *://omofuns.xyz/*
// @match *://*.omofuns.xyz/*
// @match *://www.dongmandaquan.vip/*
// @match *://*.dongmandaquan.vip/*
// @match *://senfun.in/*
// @match *://*.senfun.in/*
// @run-at document-start
// @all-frames true
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @connect *
// ==/UserScript==
(function () {
"use strict";
const PAGE = typeof unsafeWindow === "object" && unsafeWindow ? unsafeWindow : window;
const MODE_KEY = "adbandon.mode";
const MODE_FILTER = "FILTER";
const MODE_AD_ONLY = "AD_ONLY";
const CONFIG = {
minDuration: 10,
maxDuration: 30,
minSegments: 3,
maxSegments: 12,
minRepeatCount: 2,
largeNumericJump: 1000,
sparseMaxGroups: 8,
sparseLongMinDuration: 60,
sparseShortMaxLongRatio: 0.35,
};
const state = {
currentHls: null,
lastOriginalUrl: "",
lastContentUrl: "",
lastAdUrl: "",
lastReport: null,
probing: new Map(),
};
function mode() {
try {
return GM_getValue(MODE_KEY, MODE_FILTER) === MODE_AD_ONLY ? MODE_AD_ONLY : MODE_FILTER;
} catch {
return MODE_FILTER;
}
}
function setMode(nextMode) {
try {
GM_setValue(MODE_KEY, nextMode);
} catch {}
broadcastMode(nextMode);
renderWidget();
applyLatestMode();
}
function toggleMode() {
setMode(mode() === MODE_AD_ONLY ? MODE_FILTER : MODE_AD_ONLY);
}
function modeLabel() {
return mode() === MODE_AD_ONLY ? "AD" : "FILTER";
}
function renderWidget() {
if (window.top !== window) return;
const doc = document;
let button = doc.getElementById("adbandon-mode-button");
if (!button) {
button = doc.createElement("button");
button.id = "adbandon-mode-button";
button.type = "button";
button.addEventListener("click", toggleMode);
doc.documentElement.appendChild(button);
}
button.textContent = `Adbandon: ${modeLabel()}`;
button.title = mode() === MODE_AD_ONLY ? "当前只播放识别到的广告,点击切回默认过滤" : "当前默认过滤广告,点击只播放识别到的广告";
Object.assign(button.style, {
position: "fixed",
right: "14px",
bottom: "14px",
zIndex: "2147483647",
boxSizing: "border-box",
minWidth: "112px",
height: "34px",
padding: "0 12px",
border: "1px solid rgba(255,255,255,.22)",
borderRadius: "7px",
background: mode() === MODE_AD_ONLY ? "#9f1239" : "#111827",
color: "#fff",
font: "600 12px/32px system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",
letterSpacing: "0",
boxShadow: "0 6px 18px rgba(0,0,0,.24)",
cursor: "pointer",
opacity: "0.92",
});
}
function broadcastMode(nextMode) {
try {
for (const iframe of document.querySelectorAll("iframe")) {
iframe.contentWindow?.postMessage({ type: "adbandon-mode", mode: nextMode }, "*");
}
} catch {}
}
function installFrameMessaging() {
window.addEventListener("message", (event) => {
const data = event.data;
if (!data || data.type !== "adbandon-mode") return;
if (data.mode !== MODE_FILTER && data.mode !== MODE_AD_ONLY) return;
try {
GM_setValue(MODE_KEY, data.mode);
} catch {}
renderWidget();
applyLatestMode();
});
}
function log(...args) {
console.log("[Adbandon]", ...args);
}
function isM3u8(url) {
return typeof url === "string" && /\.m3u8(?:[?#]|$)/i.test(url);
}
function absolutize(baseUrl, value) {
try {
return new URL(value, baseUrl).href;
} catch {
return value;
}
}
function httpGetText(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url,
timeout: 20000,
responseType: "text",
onload: (res) => {
if (res.status >= 200 && res.status < 400) resolve(res.responseText || "");
else reject(new Error(`HTTP ${res.status} ${url}`));
},
ontimeout: () => reject(new Error(`Timeout ${url}`)),
onerror: () => reject(new Error(`Request failed ${url}`)),
});
});
}
function parseExtinf(line) {
const value = line.includes(":") ? line.split(":", 2)[1] : "";
return Number.parseFloat(value.split(",", 1)[0].trim());
}
function parseSegments(text, manifestUrl) {
const segments = [];
const stateLines = new Map();
let pendingLines = [];
let pendingDuration = null;
let pendingDiscontinuity = false;
for (const raw of text.split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
if (line === "#EXT-X-DISCONTINUITY") {
pendingDiscontinuity = true;
pendingLines.push(raw);
continue;
}
if (line.startsWith("#EXT-X-KEY") || line.startsWith("#EXT-X-MAP")) {
stateLines.set(line.split(":", 1)[0], raw);
pendingLines.push(raw);
continue;
}
if (line.startsWith("#EXT-X-BYTERANGE")) {
pendingLines.push(raw);
continue;
}
if (line.startsWith("#EXTINF:")) {
pendingDuration = parseExtinf(line);
pendingLines.push(raw);
continue;
}
if (line.startsWith("#")) continue;
if (!Number.isFinite(pendingDuration)) continue;
const uri = absolutize(manifestUrl, line);
pendingLines.push(uri);
segments.push({
duration: pendingDuration,
uri,
rawLines: pendingLines,
stateLines: new Map(stateLines),
hasDiscontinuity: pendingDiscontinuity,
});
pendingLines = [];
pendingDuration = null;
pendingDiscontinuity = false;
}
return segments;
}
function buildGroups(segments) {
const groups = [];
let current = [];
for (const segment of segments) {
if (current.length && segment.hasDiscontinuity) {
groups.push(current);
current = [];
}
current.push(segment);
}
if (current.length) groups.push(current);
return groups;
}
function topKey(map) {
let bestKey = "";
let bestCount = -1;
for (const [key, count] of map.entries()) {
if (count > bestCount) {
bestKey = key;
bestCount = count;
}
}
return bestKey;
}
function uriParts(uri) {
try {
const u = new URL(uri);
const parts = u.pathname.split("/").filter(Boolean);
const name = parts[parts.length - 1] || "";
const parent = "/" + parts.slice(0, -1).join("/");
const asset = parts.length >= 4 ? "/" + parts.slice(0, 4).join("/") : parent;
return { host: u.host, parent, asset, name };
} catch {
const name = uri.split("?")[0].split("/").pop() || "";
return { host: "", parent: "", asset: "", name };
}
}
function basename(uri) {
return uriParts(uri).name || uri;
}
function trailingNumber(name) {
const stem = name.replace(/\.[^.]+$/, "");
const match = stem.match(/(\d+)$/);
return match ? Number.parseInt(match[1], 10) : null;
}
function groupStats(group) {
const parents = new Map();
const assets = new Map();
const names = [];
for (const segment of group) {
const p = uriParts(segment.uri);
parents.set(p.parent, (parents.get(p.parent) || 0) + 1);
assets.set(p.asset, (assets.get(p.asset) || 0) + 1);
names.push(p.name);
}
return {
duration: group.reduce((sum, s) => sum + s.duration, 0),
segmentCount: group.length,
parent: topKey(parents),
asset: topKey(assets),
firstName: names[0] || "",
lastName: names[names.length - 1] || "",
firstNumber: trailingNumber(names[0] || ""),
lastNumber: trailingNumber(names[names.length - 1] || ""),
signature: group.map((s) => `${s.duration.toFixed(3)} ${basename(s.uri)}`).join("|"),
shapeSignature: "",
};
}
function isShortGroup(stats) {
return (
stats.duration >= CONFIG.minDuration &&
stats.duration <= CONFIG.maxDuration &&
stats.segmentCount >= CONFIG.minSegments &&
stats.segmentCount <= CONFIG.maxSegments
);
}
function isLongNeighbor(stats) {
return stats && stats.duration >= CONFIG.sparseLongMinDuration;
}
function detectAdGroups(groups) {
const stats = groups.map(groupStats);
for (const s of stats) {
s.shapeSignature = `${s.segmentCount}:${s.duration.toFixed(1)}`;
}
const parentCounts = new Map();
const assetCounts = new Map();
const signatureCounts = new Map();
const shapeCounts = new Map();
for (const s of stats) {
parentCounts.set(s.parent, (parentCounts.get(s.parent) || 0) + 1);
assetCounts.set(s.asset, (assetCounts.get(s.asset) || 0) + 1);
signatureCounts.set(s.signature, (signatureCounts.get(s.signature) || 0) + 1);
shapeCounts.set(s.shapeSignature, (shapeCounts.get(s.shapeSignature) || 0) + 1);
}
const dominantParent = topKey(parentCounts);
const dominantAsset = topKey(assetCounts);
const sparse = groups.length <= CONFIG.sparseMaxGroups;
return stats.map((s, index) => {
const prev = stats[index - 1] || null;
const next = stats[index + 1] || null;
const shortGroup = isShortGroup(s);
const reasons = [];
const betweenLong = shortGroup && isLongNeighbor(prev) && isLongNeighbor(next);
const smallIsland =
betweenLong &&
s.duration <= Math.min(prev.duration, next.duration) * CONFIG.sparseShortMaxLongRatio;
if (shortGroup && (signatureCounts.get(s.signature) || 0) >= CONFIG.minRepeatCount) {
reasons.push("repeated-signature");
}
if (
sparse &&
smallIsland &&
(shapeCounts.get(s.shapeSignature) || 0) >= CONFIG.minRepeatCount
) {
reasons.push("sparse-repeated-shape-island");
}
if (sparse && groups.length <= 3 && smallIsland) {
reasons.push("sparse-single-shape-island");
}
if (
shortGroup &&
s.parent !== dominantParent &&
prev &&
next &&
prev.parent === dominantParent &&
next.parent === dominantParent
) {
reasons.push("minority-parent-island");
}
if (
shortGroup &&
s.asset !== dominantAsset &&
prev &&
next &&
prev.asset === dominantAsset &&
next.asset === dominantAsset
) {
reasons.push("minority-asset-island");
}
if (
shortGroup &&
s.parent === dominantParent &&
prev &&
next &&
prev.parent === dominantParent &&
next.parent === dominantParent &&
Number.isFinite(prev.lastNumber) &&
Number.isFinite(s.firstNumber) &&
Number.isFinite(next.firstNumber) &&
Math.abs(s.firstNumber - prev.lastNumber) >= CONFIG.largeNumericJump &&
Math.abs(next.firstNumber - prev.lastNumber) <= 1
) {
reasons.push("numeric-jump-island");
}
return { ...s, index, remove: reasons.length > 0, reasons };
});
}
function extractHeaderLines(text) {
const out = [];
const seen = new Set();
for (const raw of text.split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
if (line === "#EXT-X-DISCONTINUITY" || line.startsWith("#EXTINF:")) break;
if (!line.startsWith("#")) break;
if (
line.startsWith("#EXT-X-KEY") ||
line.startsWith("#EXT-X-MAP") ||
line.startsWith("#EXT-X-BYTERANGE") ||
line === "#EXT-X-ENDLIST"
) {
continue;
}
if (seen.has(line)) continue;
seen.add(line);
out.push(raw);
}
return out.length ? out : ["#EXTM3U"];
}
function buildManifest(text, groups, decisions, keepAds) {
const out = extractHeaderLines(text);
const outputState = new Map();
let wroteAny = false;
for (const decision of decisions) {
if (keepAds !== decision.remove) continue;
const group = groups[decision.index];
if (!group) continue;
if (wroteAny) out.push("#EXT-X-DISCONTINUITY");
for (const segment of group) {
for (const [stateKey, stateLine] of segment.stateLines.entries()) {
if (outputState.get(stateKey) !== stateLine) {
out.push(stateLine);
outputState.set(stateKey, stateLine);
}
}
for (const line of segment.rawLines) {
const trimmed = String(line).trim();
if (
trimmed === "#EXT-X-DISCONTINUITY" ||
trimmed.startsWith("#EXT-X-KEY") ||
trimmed.startsWith("#EXT-X-MAP")
) {
continue;
}
out.push(line);
}
}
wroteAny = true;
}
if (!wroteAny) return "";
out.push("#EXT-X-ENDLIST");
return out.join("\n") + "\n";
}
function pickVariantUrl(text, manifestUrl) {
const lines = text.split(/\r?\n/);
for (let i = 0; i < lines.length; i += 1) {
if (!lines[i].trim().startsWith("#EXT-X-STREAM-INF")) continue;
for (let j = i + 1; j < lines.length; j += 1) {
const line = lines[j].trim();
if (!line || line.startsWith("#")) continue;
return absolutize(manifestUrl, line);
}
}
return "";
}
function revokeUrl(url) {
if (url) URL.revokeObjectURL(url);
}
async function analyzeUrl(url, depth = 0) {
const text = await httpGetText(url);
if (!text.includes("#EXTM3U")) return null;
if (text.includes("#EXT-X-STREAM-INF") && depth < 2) {
const variant = pickVariantUrl(text, url);
return variant ? analyzeUrl(variant, depth + 1) : null;
}
if (!text.includes("#EXT-X-ENDLIST") || !text.includes("#EXT-X-DISCONTINUITY")) return null;
const segments = parseSegments(text, url);
const groups = buildGroups(segments);
if (groups.length < 3) return null;
const decisions = detectAdGroups(groups);
const candidateIndexes = decisions.filter((d) => d.remove).map((d) => d.index);
if (!candidateIndexes.length) return null;
const contentManifest = buildManifest(text, groups, decisions, false);
const adManifest = buildManifest(text, groups, decisions, true);
if (!contentManifest || !adManifest) return null;
return {
url,
groupCount: groups.length,
candidateIndexes,
decisions,
contentManifest,
adManifest,
};
}
function createManifestUrl(text) {
return URL.createObjectURL(new Blob([text], { type: "application/vnd.apple.mpegurl" }));
}
function loadPlaybackUrl(url) {
if (!url) return;
const video = PAGE.document?.querySelector?.("video") || document.querySelector("video");
if (state.currentHls && typeof state.currentHls.loadSource === "function") {
state.currentHls.loadSource(url);
if (video && typeof state.currentHls.attachMedia === "function") state.currentHls.attachMedia(video);
video?.play?.().catch(() => {});
return;
}
if (video) {
video.src = url;
video.play?.().catch(() => {});
}
}
function applyLatestMode() {
if (!state.lastReport) return;
const target = mode() === MODE_AD_ONLY ? state.lastAdUrl : state.lastContentUrl;
loadPlaybackUrl(target);
}
function enqueueProbe(url, reason) {
if (!isM3u8(url) || String(url).startsWith("blob:")) return;
const absolute = absolutize(PAGE.location?.href || location.href, url);
if (state.probing.has(absolute)) return;
const promise = analyzeUrl(absolute)
.then((report) => {
state.probing.delete(absolute);
if (!report) return null;
revokeUrl(state.lastContentUrl);
revokeUrl(state.lastAdUrl);
state.lastOriginalUrl = absolute;
state.lastReport = report;
state.lastContentUrl = createManifestUrl(report.contentManifest);
state.lastAdUrl = createManifestUrl(report.adManifest);
log("matched", reason, {
url: report.url,
groups: report.groupCount,
candidates: report.candidateIndexes,
reasons: report.decisions.filter((d) => d.remove).map((d) => [d.index, d.reasons]),
mode: mode(),
});
applyLatestMode();
return report;
})
.catch((error) => {
state.probing.delete(absolute);
log("probe failed", reason, absolute, error);
return null;
});
state.probing.set(absolute, promise);
}
function installHlsHook() {
const timer = window.setInterval(() => {
const Hls = PAGE.Hls;
if (!Hls || Hls.__adbandonHooked) return;
Hls.__adbandonHooked = true;
const originalLoadSource = Hls.prototype.loadSource;
Hls.prototype.loadSource = function (url) {
state.currentHls = this;
enqueueProbe(url, "hls.loadSource");
return originalLoadSource.apply(this, arguments);
};
window.clearInterval(timer);
}, 200);
}
function installVideoHook() {
const ElementPrototype = PAGE.Element?.prototype || Element.prototype;
const HTMLMediaElementPrototype = PAGE.HTMLMediaElement?.prototype || HTMLMediaElement.prototype;
const HTMLVideoElementClass = PAGE.HTMLVideoElement || HTMLVideoElement;
const originalSetAttribute = ElementPrototype.setAttribute;
ElementPrototype.setAttribute = function (name, value) {
if (this instanceof HTMLVideoElementClass && String(name).toLowerCase() === "src") {
enqueueProbe(value, "video.setAttribute");
}
return originalSetAttribute.apply(this, arguments);
};
const descriptor = Object.getOwnPropertyDescriptor(HTMLMediaElementPrototype, "src");
if (descriptor?.set && descriptor?.get) {
Object.defineProperty(HTMLMediaElementPrototype, "src", {
configurable: true,
enumerable: descriptor.enumerable,
get: descriptor.get,
set(value) {
if (this instanceof HTMLVideoElementClass) enqueueProbe(value, "video.src");
return descriptor.set.call(this, value);
},
});
}
}
function installNetworkHooks() {
if (PAGE.__adbandonNetworkHooked) return;
PAGE.__adbandonNetworkHooked = true;
const originalFetch = PAGE.fetch;
if (typeof originalFetch === "function") {
PAGE.fetch = function (input, init) {
const url = typeof input === "string" ? input : input?.url;
enqueueProbe(url, "fetch");
return originalFetch.apply(this, arguments).then((response) => {
enqueueProbe(response?.url || url, "fetch.response");
return response;
});
};
}
const xhrPrototype = PAGE.XMLHttpRequest?.prototype;
if (xhrPrototype) {
const originalOpen = xhrPrototype.open;
xhrPrototype.open = function (_method, url) {
this.__adbandonUrl = url;
enqueueProbe(url, "xhr.open");
return originalOpen.apply(this, arguments);
};
const originalSend = xhrPrototype.send;
xhrPrototype.send = function () {
this.addEventListener("load", () => enqueueProbe(this.responseURL || this.__adbandonUrl, "xhr.load"));
return originalSend.apply(this, arguments);
};
}
}
function scanKnownPlaces() {
try {
for (const entry of PAGE.performance?.getEntriesByType?.("resource") || []) {
enqueueProbe(entry?.name, "performance");
}
} catch {}
try {
for (const video of PAGE.document?.querySelectorAll?.("video") || []) {
enqueueProbe(video.currentSrc, "video.currentSrc");
enqueueProbe(video.src, "video.src.scan");
}
} catch {}
for (const key of ["player_aaaa", "player_data", "MacPlayer"]) {
try {
const value = PAGE[key];
if (!value) continue;
for (const prop of ["url", "PlayUrl", "playUrl", "src"]) {
enqueueProbe(value[prop], `${key}.${prop}`);
}
} catch {}
}
}
function installResourceObserver() {
try {
const observer = new PAGE.PerformanceObserver((list) => {
for (const entry of list.getEntries()) enqueueProbe(entry?.name, "performance-observer");
});
observer.observe({ type: "resource", buffered: true });
} catch {}
window.setInterval(scanKnownPlaces, 2000);
}
renderWidget();
installFrameMessaging();
installHlsHook();
installVideoHook();
installNetworkHooks();
installResourceObserver();
window.setTimeout(scanKnownPlaces, 1000);
})();