JavScript

暗黑模式、滚动加载、在线播放、磁力搜索、115资源匹配 & 离线下载、预览视频、预览大图...

As of 2022-01-13. See the latest version.

// ==UserScript==
// @name            JavScript
// @namespace       https://greasyfork.org/users/175514
// @description     暗黑模式、滚动加载、在线播放、磁力搜索、115资源匹配 & 离线下载、预览视频、预览大图...
// @version         1.6.0
// @icon            https://z3.ax1x.com/2021/10/15/53gMFS.png
// @include         *
// @require         https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js
// @require         https://unpkg.com/infinite-scroll@4/dist/infinite-scroll.pkgd.min.js
// @resource        fail https://z3.ax1x.com/2021/10/15/53gcex.png
// @resource        info https://z3.ax1x.com/2021/10/15/53g2TK.png
// @resource        success https://z3.ax1x.com/2021/10/15/53gqTf.png
// @resource        play https://s4.ax1x.com/2022/01/12/7nYuKe.png
// @resource        loading https://via.placeholder.com/824x40
// @run-at          document-start
// @grant           GM_registerMenuCommand
// @grant           GM_getResourceURL
// @grant           GM_xmlhttpRequest
// @grant           GM_setClipboard
// @grant           GM_notification
// @grant           GM_openInTab
// @grant           GM_addStyle
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           GM_info
// @connect         *
// @license         MIT
// ==/UserScript==

(function () {
	// domains
	const createReg = items => new RegExp(`(${items.join("|").replace(/\./g, "\\.")})+`, "gi");
	const MatchDomains = [
		{
			domain: "JavBus",
			regex: createReg([
				"javbus.com",
				"dmmbus.fun",
				"dmmsee.fun",
				"busjav.fun",
				"busjav.bar",
				"cdnbus.fun",
				"busfan.fun",
				"seedmm.fun",
				"busdmm.fun",
				"buscdn.fun",
			]),
		},
		// {
		// 	domain: "JavDB",
		// 	regex: createReg(["javdb.com", "javdb35.com"]),
		// },
	];
	// document
	const DOC = document;
	DOC.create = (tag, attr = {}, child = "") => {
		if (!tag) return null;
		tag = DOC.createElement(tag);
		Object.keys(attr).forEach(name => tag.setAttribute(name, attr[name]));
		typeof child === "string" && tag.appendChild(DOC.createTextNode(child));
		typeof child === "object" && tag.appendChild(child);
		return tag;
	};
	// request
	const request = (url, data = {}, method = "GET", params = {}) => {
		if (typeof data === "object") {
			data = Object.keys(data).reduce(
				(pre, cur) => `${pre ? `${pre}&` : ""}${cur}=${encodeURIComponent(data[cur])}`,
				""
			);
		}
		if (method === "GET" && data) url = `${url}?${data}`;
		return new Promise(resolve => {
			GM_xmlhttpRequest({
				url,
				data,
				method,
				timeout: 20000,
				onload: ({ status, response }) => {
					if (status === 404) resolve(false);
					const htmlReg = /<\/?[a-z][\s\S]*>/i;
					const jsonReg = /^{.*}$/;
					if (htmlReg.test(response)) {
						response = new DOMParser().parseFromString(response, "text/html");
					} else if (jsonReg.test(response)) {
						response = JSON.parse(response);
					}
					if (response?.errcode === 911) verify();
					resolve(response);
				},
				...params,
			});
		});
	};
	const verify = () => {
		const h = 667;
		const w = 375;
		const t = (window.screen.availHeight - h) / 2;
		const l = (window.screen.availWidth - w) / 2;
		window.open(
			`https://captchaapi.115.com/?ac=security_code&type=web&cb=Close911_${new Date().getTime()}`,
			"验证账号",
			`height=${h},width=${w},top=${t},left=${l},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
		);
	};
	const notify = msg => {
		GM_notification({
			...msg,
			text: msg?.text ?? GM_info.script.name,
			image: GM_getResourceURL(msg?.image ?? "info"),
			onclick: msg?.clickUrl ? () => GM_openInTab(msg.clickUrl, { active: true }) : () => {},
			highlight: true,
			timeout: 3000,
		});
	};
	const getDate = timestamp => {
		const date = timestamp ? new Date(timestamp) : new Date();
		const Y = date.getFullYear();
		const M = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
		const D = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
		return `${Y}-${M}-${D}`;
	};
	const getScrollTop = () => {
		let scrollTop = 0;
		if (DOC.documentElement && DOC.documentElement.scrollTop) {
			scrollTop = DOC.documentElement.scrollTop;
		} else if (DOC.body) {
			scrollTop = DOC.body.scrollTop;
		}
		return scrollTop;
	};
	const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
	const getItem = key => {
		const details = GM_getValue("DETAILS") ?? {};
		return details[key] ?? {};
	};
	const upItem = (key, val) => {
		const details = GM_getValue("DETAILS") ?? {};
		val = { ...getItem(key), ...val };
		details[key] = val;
		GM_setValue("DETAILS", details);
	};
	const transToBytes = sizeStr => {
		const limit = 1024;
		const step = 8;
		const sizer = [
			{
				unit: /gb/gi,
				transform: size => size * limit * limit * limit,
			},
			{
				unit: /gib/gi,
				transform: size => (size / step) * limit * limit * limit,
			},
			{
				unit: /mb/gi,
				transform: size => size * limit * limit,
			},
			{
				unit: /mib/gi,
				transform: size => (size / step) * limit * limit,
			},
			{
				unit: /kb/gi,
				transform: size => size * limit,
			},
			{
				unit: /kib/gi,
				transform: size => (size / step) * limit,
			},
			{
				unit: /byte/gi,
				transform: size => size,
			},
			{
				unit: /bit/gi,
				transform: size => size / step,
			},
		];
		const size = sizeStr.replace(/[a-zA-Z\s]/g, "");
		if (size <= 0) return 0;
		return (
			sizer
				.find(({ unit }) => unit.test(sizeStr))
				?.transform(size)
				?.toFixed(2) ?? 0
		);
	};
	const unique = (arr, key) => {
		if (!arr) return arr;
		if (key === undefined) return [...new Set(arr)];
		const map = { string: e => e[key], function: e => key(e) };
		const fn = map[typeof key];
		const obj = arr.reduce((o, e) => ((o[fn(e)] = e), o), {});
		return Object.values(obj);
	};

	class Common {
		docStart = () => {};
		contentLoaded = () => {};
		load = () => {};

		mPoint = 0;
		pcPreview = "https://v.anxia.com/?pickcode=";
		menus = [
			{ title: "点击事件", key: "CK", command: "c" },
			{ title: "暗黑模式", key: "DM", command: "d" },
			{ title: "滚动加载", key: "LM", command: "s" },
			{
				title: "离线后操作目录cid",
				key: "CID",
				command: "a",
				cb: uniKey => {
					const val = prompt("用于离线下载后移动,删除操作", GM_getValue(uniKey) ?? "");
					if (!val) return;
					GM_setValue(uniKey, val);
					location.reload();
				},
			},
		];
		registerMenu = (menus = this.menus) => {
			for (const menu of menus) {
				const { title, key, command } = menu;
				const uniKey = `${this.constructor.name}_${key}`;
				const val = GM_getValue(`${uniKey}`) ?? "";
				GM_registerMenuCommand(
					`${menu?.cb ? "设置" : val ? "关闭" : "开启"}${title}`,
					menu?.cb
						? () => menu.cb(uniKey)
						: () => {
								GM_setValue(uniKey, !val);
								location.reload();
						  },
					command
				);
				this[key] = val;
			}
		};
		initDB = () => {
			const date = getDate();
			const rcdKey = `CD`;
			if (GM_getValue(rcdKey) !== date) {
				GM_setValue("DETAILS", {});
				GM_setValue("RESOURCE", []);
				GM_setValue(rcdKey, date);
			}
		};
		customStyle = () => {
			GM_addStyle(`
            video, img { vertical-align: middle !important; }
            .ellipsis {
                overflow : hidden;
                text-overflow: ellipsis;
                display: -webkit-box;
                -webkit-line-clamp: 1;
                -webkit-box-orient: vertical;
            }
            .line-4 { -webkit-line-clamp: 4; }
            .playBtn { position: relative; }
            .playBtn:after {
                position: absolute;
                background: url(${GM_getResourceURL("play")}) 50% no-repeat;
                background-color: rgba(0,0,0,.2);
                background-size: 40px;
                content: "";
                top: 0;
                right: 0;
                bottom: 0;
                left: 0;
                transition: all .3s ease-out;
                opacity: .85;
            }
            .playBtn:hover::after { background-color: rgba(0,0,0,0); }
            .playBtn img { filter: none !important; }
            .mask {
                position: fixed;
                width: 100%;
                height: 100%;
                z-index: 9999;
                left: 0;
                top: 0;
                background: rgba(11, 11, 11, .8);
                justify-content: center;
                align-items: center;
                display: none;
            }
            .matched {
                color: rgb(10,132,255) !important;
                font-weight: bold;
            }
            `);
		};
		darkStyle = () => {
			const Background = "rgb(18,18,18)";
			const SecondaryBackground = "rgb(32,32,32)";
			const LabelColor = "rgba(255,255,255,.95)";
			const SecondaryLabelColor = "rgb(170,170,170)";
			const Grey = "rgb(49,49,49)";
			const Blue = "rgb(10,132,255)";
			const Orange = "rgb(255,159,10)";
			const Green = "rgb(48,209,88)";
			const Red = "rgb(255,69,58)";
			GM_addStyle(`
            ::-webkit-scrollbar {
                width: 8px;
                height: 8px;
            }
            ::-webkit-scrollbar-thumb {
                border-radius: 4px;
                background-color: ${Grey};
            }
            *:not(span) {
                border-color: ${Grey} !important;
                outline: ${Grey} !important;
                text-shadow: none !important;
            }
            body, footer {
                background: ${Background} !important;
                color: ${SecondaryLabelColor} !important;
            }
            img { filter: brightness(90%) !important; }
            nav { background: ${SecondaryBackground} !important; }
            input { background: ${Background} !important; }
            *::placeholder { color: ${SecondaryLabelColor} !important; }
            button, input, a, h1, h2, h3, h4, h5, h6, p {
                color: ${LabelColor} !important;
                box-shadow: none !important;
                outline: none !important;
            }
            `);
			return {
				Background,
				SecondaryBackground,
				LabelColor,
				SecondaryLabelColor,
				Grey,
				Blue,
				Orange,
				Green,
				Red,
			};
		};
		handleClick = (selectors, node = DOC) => {
			for (const item of node.querySelectorAll(selectors)) {
				const href = item?.href;
				if (!href) continue;
				item.addEventListener("contextmenu", e => e.preventDefault());
				item.addEventListener("click", e => {
					e.preventDefault();
					GM_openInTab(href, { active: true });
				});
				item.addEventListener("mousedown", e => {
					if (e.button !== 2) return;
					e.preventDefault();
					this.mPoint = e.screenX + e.screenY;
				});
				item.addEventListener("mouseup", e => {
					const num = e.screenX + e.screenY - this.mPoint;
					if (e.button !== 2 || num > 5 || num < -5) return;
					e.preventDefault();
					GM_openInTab(href);
				});
			}
		};
		waterfallLayout = (selectors, itemSelectors, mParams = {}, iParams = {}) => {
			const node = DOC.querySelector(selectors);
			if (!node) return;
			const msnry = new Masonry(node, {
				itemSelector: "none",
				columnWidth: ".item-sizer",
				gutter: ".gutter-sizer",
				horizontalOrder: true,
				fitWidth: true,
				stagger: 30,
				visibleStyle: { transform: "translateY(0)", opacity: 1 },
				hiddenStyle: { transform: "translateY(120px)", opacity: 0 },
				...mParams,
			});
			imagesLoaded(node, () => {
				msnry.options.itemSelector = itemSelectors;
				const items = node.querySelectorAll(itemSelectors);
				msnry.appended(items);
				GM_addStyle(`${selectors} { opacity: 1; }`);
			});
			const infScroll = new InfiniteScroll(node, {
				path: "#next",
				append: itemSelectors,
				outlayer: msnry,
				elementScroll: ".scrollBox",
				history: false,
				historyTitle: false,
				hideNav: ".pagination",
				status: ".page-load-status",
				debug: false,
				...iParams,
			});
			return infScroll;
		};
		copyTxt = e => {
			e.preventDefault();
			e.stopPropagation();
			const { target: node } = e;
			if (!node) return;
			const { copy } = node.dataset;
			if (!copy) return;
			GM_setClipboard(copy);
			const initial = node?.textContent ?? "";
			node.textContent = "成功";
			setTimeout(() => {
				node.textContent = initial;
			}, 600);
		};
		fetchMatch = async code => {
			let res = getItem(code)?.resource;
			if (!res) {
				const resource = GM_getValue("RESOURCE") ?? [];
				const prefix = code.split(/(-|_)/g)[0];
				let item = resource.find(item => item.prefix === prefix);
				if (!item) {
					item = { prefix, res: await this.fetchSearch(prefix) };
					resource.push(item);
					GM_setValue("RESOURCE", resource);
				}
				res = await this.fetchResource(code, item.res);
			}
			return res;
		};
		fetchSearch = async search_value => {
			const res = await request(
				"https://webapi.115.com/files/search",
				{
					search_value,
					offset: 0,
					limit: 10000,
					date: "",
					aid: 1,
					cid: 0,
					pick_code: "",
					type: 4,
					source: "",
					format: "json",
					o: "user_ptime",
					asc: 0,
					star: "",
					suffix: "",
				},
				"GET",
				{ responseType: "json" }
			);
			return (res?.data ?? []).map(({ fid, cid, n, pc, t, te, tp, play_long }) => {
				return { fid, cid, n, pc, t, te, tp, play_long };
			});
		};
		fetchResource = async (code = "", res) => {
			if (!res) res = await this.fetchSearch(code.split(/(-|_)/g)[0]);
			if (res?.length) {
				const codes = unique([
					code,
					code.replace(/-/g, ""),
					code.replace(/-/g, "."),
					code.replace(/-0/g, "."),
					code.replace(/-/g, "-0"),
					code.replace(/-/g, "0"),
					code.replace(/-/g, "00"),
					code.replace(/-/g, "_"),
					code.replace(/-/g, "_0"),
					code.replace(/-0/g, ""),
					code.replace(/-0/g, "-"),
					code.replace(/-0/g, "00"),
				]);
				const reg = createReg(codes);
				res = res
					.filter(({ n, play_long }) => n.match(reg) && play_long > 0)
					.map(({ n: name, pc: pickCode, fid, cid, te, tp, t: date }) => {
						return { name, pickCode, fid, cid, timestamp: Math.max(te, tp), date };
					});
			}
			return res;
		};
		fetchStar = async code => {
			const site = "https://javdb.com";
			let res = await request(`${site}/search?q=${code}`);
			const href = res?.querySelector("#videos .grid-item a").getAttribute("href");
			if (!href) return;
			res = await request(`${site}${href}`);
			res = res?.querySelectorAll(".panel-block");
			if (!res?.length) return;
			res = res[res.length - 3]?.querySelector(".value").textContent.trim();
			return res
				.split(/\n/)
				.filter(item => item.indexOf("♀") !== -1)
				.map(item => item.replace("♀", "").trim());
		};
		fetchImage = async (code, date) => {
			date = date.split("-");
			const jb = `http://img.japanese-bukkake.net/${date[0]}/${date[1]}/${code}_s.jpg`;
			const js = `https://javstore.net/search/${code}.html`;
			let [jbRes, jsRes] = await Promise.all([request(jb), request(js)]);
			const href = jsRes?.querySelector("#content_news li a")?.href;
			if (href) {
				jsRes = await request(href);
				jsRes = jsRes?.querySelector(".news a img[alt*='.th']").src.replace(".th", "");
				if (jsRes && (await request(jsRes))) return jsRes;
			}
			if (typeof jbRes === "object") return jb;
		};
		fetchVideo = async ({ code, studio }) => {
			code = code.toLowerCase();
			if (studio) {
				const fetchVideoByStudio = [
					{ name: "東京熱", match: "https://my.cdn.tokyo-hot.com/media/samples/%s.mp4" },
					{ name: "カリビアンコム", match: "https://smovie.caribbeancom.com/sample/movies/%s/1080p.mp4" },
					{ name: "一本道", match: "http://smovie.1pondo.tv/sample/movies/%s/1080p.mp4" },
					{
						name: "HEYZO",
						transform: code => code.replace(/HEYZO\-/gi, ""),
						match: "https://www.heyzo.com/contents/3000/%s/heyzo_hd_%s_sample.mp4",
					},
				];
				const matched = fetchVideoByStudio.find(({ name }) => name === studio);
				if (matched) return matched.match.replace(/%s/g, matched.transform ? matched.transform(code) : code);
			}
			const [r18, xrmoo] = await Promise.all([
				request(`https://www.r18.com/common/search/searchword=${code}/`),
				request(`http://dmm.xrmoo.com/sindex.php?searchstr=${code}`),
			]);
			return (
				r18?.querySelector("a.js-view-sample")?.getAttribute("data-video-high") ||
				xrmoo
					?.querySelector(".card .card-footer a.viewVideo")
					?.getAttribute("data-link")
					.replace("_sm_w", "_dmb_w") ||
				""
			);
		};
		fetchMagnet = async code => {
			const matchList = [
				{
					site: "Sukebei",
					host: "https://sukebei.nyaa.si/",
					search: "?f=0&c=0_0&q=%s",
					selectors: ".table-responsive table tbody tr",
					filter: {
						name: e => e?.querySelector("td:nth-child(2) a").textContent,
						link: e => e?.querySelector("td:nth-child(3) a:last-child").href,
						size: e => e?.querySelector("td:nth-child(4)").textContent,
						date: e => e?.querySelector("td:nth-child(5)").textContent.split(" ")[0],
						href: e => e?.querySelector("td:nth-child(2) a").getAttribute("href"),
					},
					styleClass: "btn-primary",
				},
				{
					site: "BTGG",
					host: "https://www.btgg.cc/",
					search: "search?q=%s",
					selectors: ".el-main .list .item",
					filter: {
						name: e => e?.querySelector(".name a").textContent,
						link: e => e?.querySelector(".meta a").href,
						size: e => e?.querySelector(".meta span").textContent.split(",")[0],
						date: e => e?.querySelector(".meta span:nth-child(4)").textContent.split(",")[0],
						href: e => e?.querySelector(".name a").getAttribute("href"),
					},
					styleClass: "btn-success",
				},
				{
					site: "BTSOW",
					host: "https://btsow.rest/",
					search: "search/%s",
					selectors: ".data-list .row:not(.hidden-xs)",
					filter: {
						name: e => e?.querySelector(".file").textContent,
						link: e => `magnet:?xt=urn:btih:${e?.querySelector("a").href.split("/").pop()}`,
						size: e => e?.querySelector(".size").textContent,
						date: e => e?.querySelector(".date").textContent,
						href: e => e?.querySelector("a").getAttribute("href"),
					},
					styleClass: "btn-danger",
				},
			];
			const matched = await Promise.all(
				matchList.map(({ host, search }) => request(`${host}${search.replace(/%s/g, code)}`))
			);
			const magnets = [];
			for (let index = 0; index < matchList.length; index++) {
				let node = matched[index];
				if (!node) continue;
				const { selectors, site, styleClass, filter, host } = matchList[index];
				node = node?.querySelectorAll(selectors);
				if (!node || !node?.length) continue;
				for (const item of node) {
					const magnet = { from: site, styleClass };
					Object.keys(filter).map(key => {
						magnet[key] = filter[key](item).trim();
					});
					if (!("zh" in magnet)) magnet.zh = /中文/g.test(magnet.name);
					magnet.link = magnet.link.split("&")[0];
					const href = magnet?.href ?? "";
					if (href && !href.includes("//")) magnet.href = `${host}${href.replace(/^\//, "")}`;
					magnets.push(magnet);
				}
			}
			return magnets;
		};
		fetchPlayer = async code => {
			const codeReg = new RegExp(code, "gi");
			const matchList = [
				{
					site: "Netflav",
					host: "https://netflav.com/",
					search: "search?type=title&keyword=%s",
					selectors: ".grid_root .grid_cell",
					filter: { name: e => e?.querySelector(".grid_title").textContent },
				},
				{
					site: "BestJavPorn",
					host: "https://www2.bestjavporn.com/",
					search: "search/%s/",
					selectors: "#main article",
					filter: {
						name: e => e?.querySelector("a").title,
						zh: e => /中文/g.test(e?.querySelector(".hd-video")?.textContent ?? ""),
					},
				},
				{
					site: "JavHHH",
					host: "https://javhhh.com/",
					search: "v/?wd=%s",
					selectors: "#wrapper .typelist .i-container",
					filter: { name: e => e?.querySelector("img.img-responsive").title },
				},
				{
					site: "Avgle",
					host: "https://avgle.com/",
					search: "search/videos?search_query=%s&search_type=videos",
					selectors: ".row .well.well-sm",
					filter: { name: e => e?.querySelector(".video-title")?.textContent },
				},
			];
			const matched = await Promise.all(
				matchList.map(({ host, search }) => request(`${host}${search.replace(/%s/g, code)}`))
			);
			const playList = [];
			for (let index = 0; index < matchList.length; index++) {
				let node = matched[index];
				if (!node) continue;
				const { selectors, site, filter, host } = matchList[index];
				node = node?.querySelectorAll(selectors);
				if (!node || !node?.length) continue;
				for (const item of node) {
					const player = { from: site };
					for (const key of Object.keys(filter)) player[key] = filter[key](item);
					const name = player?.name ?? "";
					if (!name || !name.match(codeReg)?.length) continue;
					if (!("zh" in player)) player.zh = /中文/g.test(name);
					if (!player?.link) player.link = item?.querySelector("a").getAttribute("href");
					const link = player?.link ?? "";
					if (link && !link.includes("//")) player.link = `${host}${link.replace(/^\//, "")}`;
					playList.push(player);
				}
			}
			return playList;
		};
		fetchSign = async () => {
			const res = await request("http://115.com/", { ct: "offline", ac: "space", _: new Date().getTime() });
			if (res?.sign) return { sign: res.sign, time: res.time };
			notify({
				title: "请求失败,115未登录",
				text: "请登录115账户后再离线下载",
				image: "fail",
				clickUrl: "http://115.com/?mode=login",
			});
		};
		fetchOffLine = async url => {
			let res = await this.fetchSign();
			if (!res) return;
			return await request(
				"http://115.com/web/lixian/?ct=lixian&ac=add_task_url",
				{ url, uid: 0, ...res },
				"POST",
				{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
			);
		};
		fetchRename = async res => {
			const data = {};
			for (const { fid, file_name } of res) data[`files_new_name[${fid}]`] = file_name;
			return await request("https://webapi.115.com/files/batch_rename", data, "POST", {
				headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
				responseType: "json",
			});
		};
		fetchMove = async res => {
			const data = { pid: this.CID, move_proid: `${new Date()}_${~(100 * Math.random())}_0` };
			for (let index = 0; index < res.length; index++) data[`fid[${index}]`] = res[index].fid;
			return await request("https://webapi.115.com/files/move", data, "POST", {
				headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
				responseType: "json",
			});
		};
		fetchDelDir = async res => {
			const data = { pid: this.CID, ignore_warn: 1 };
			res = unique(res.map(({ cid }) => cid).filter(item => item !== this.CID));
			for (let index = 0; index < res.length; index++) data[`fid[${index}]`] = res[index];
			return await request("https://webapi.115.com/rb/delete", data, "POST", {
				headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
				responseType: "json",
			});
		};
	}
	class JavBus extends Common {
		constructor() {
			super();
			this.start();
			const tag = Object.keys(this.routeReg).find(key => this.routeReg[key].test(location.pathname));
			return { ...this, ...this[tag] };
		}
		maxHei = 600;
		routeReg = {
			waterfall:
				/^\/((uncensored|uncensored\/)?(page\/\d+)?$)|((uncensored\/)?((search|searchstar|actresses|genre|star|studio|label|series|director|member)+\/)|actresses(\/\d+)?)+/i,
			genre: /^\/(uncensored\/)?genre$/i,
			forum: /^\/forum\//i,
			details: /^\/[\w]+(-|_)?[\d]*.*$/i,
		};
		start = () => {
			this.registerMenu();
			this.initDB();
			this.customStyle();
			this.customMode();
			DOC.addEventListener("DOMContentLoaded", () => {
				DOC.addEventListener("keyup", event => {
					const e = event || window.event;
					if (e && e.keyCode === 191 && !["INPUT", "TEXTAREA"].includes(DOC.activeElement.nodeName)) {
						DOC.querySelector("#search-input").focus();
					}
				});
			});
		};
		customMode = () => {
			GM_addStyle(`.ad-box { display: none !important; }`);
		};
		darkMode = dStyle => {
			const { Grey, LabelColor, Blue, Green, Red, Orange, Background, SecondaryBackground } = dStyle;
			GM_addStyle(`
            .btn.disabled, .btn[disabled], fieldset[disabled] .btn { opacity: .85 !important; }
            button, .btn-default, .input-group-addon {
                background: ${Grey} !important;
                color: ${LabelColor} !important;
            }
            .btn-primary {
                background: ${Blue} !important;
                border-color: ${Blue} !important;
            }
            .btn-success {
                background: ${Green} !important;
                border-color: ${Green} !important;
            }
            .btn-danger {
                background: ${Red} !important;
                border-color: ${Red} !important;
            }
            .btn-warning {
                background: ${Orange} !important;
                border-color: ${Orange} !important;
            }
            .navbar-nav>.active>a, .navbar-nav>.active>a:focus, .navbar-nav>.active>a:hover, .navbar-nav>.open>a, .dropdown-menu { background: ${Background} !important; }
            .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover { background: ${Grey} !important; }
            .pagination .active a {
                border: none !important;
                color: ${LabelColor} !important;
            }
            .pagination>li>a, .pagination>li>span {
                background-color: ${Grey} !important;
                border: none !important;
                color: ${LabelColor} !important;
            }
            tr, .modal-content, .alert {
                background: ${SecondaryBackground} !important;
                box-shadow: none !important;
            }
            tr:hover { background: ${Grey} !important; }
            `);
		};
		waterfall = {
			docStart() {
				GM_addStyle(`
                .search-header {
                    padding: 0 !important;
                    background: none !important;
                    box-shadow: none !important;
                }
                .photo-frame {
                    position: relative;
                    margin: 10px !important;
                }
                .photo-frame img {
                    height: 100% !important;
                    width: 100% !important;
                    object-fit: cover !important;
                    margin: 0 !important;
                }
                .photo-info { padding: 10px !important; }
                .alert-page { margin: 20px !important; }
                `);
				if (this.LM) {
					const itemSizer = `167px`;
					const gutterSizer = `20px`;
					GM_addStyle(`
                    .pagination, footer { display: none !important; }
                    .page-load-status {
                        display: none;
                        padding-bottom: ${gutterSizer};
                        text-align: center;
                    }
                    body { overflow: hidden; }
                    .scrollBox {
                        height: calc(100vh - 50px);
                        overflow: hidden scroll;
                    }
                    #waterfall {
                        opacity: 0;
                        margin: ${gutterSizer} auto 0 auto !important;
                    }
                    .item-sizer, .item a { width: ${itemSizer} !important; }
                    .gutter-sizer { width: ${gutterSizer} !important; }
                    .item a { margin: 0 0 ${gutterSizer} 0 !important; }
                    `);
				}
				if (this.DM) {
					const dStyle = this.darkStyle();
					this.darkMode(dStyle);
					const { SecondaryBackground, LabelColor, SecondaryLabelColor, Grey } = dStyle;
					GM_addStyle(`
                    .item a { background: ${SecondaryBackground} !important; }
                    .photo-info {
                        background: ${SecondaryBackground} !important;
                        color: ${LabelColor} !important;
                    }
                    date { color: ${SecondaryLabelColor} !important; }
                    .nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover, .nav-pills>li>a:focus, .nav-pills>li>a:hover { background-color: ${Grey} !important; }
                    `);
				}
			},
			contentLoaded() {
				const nav = DOC.querySelector(".search-header .nav");
				if (nav) nav.setAttribute("class", "nav nav-pills");
				this.modifyItem();
				if (!this.LM) return;
				this.modifyLayout();
				this.handleWaterfall();
			},
			async modifyItem(node = DOC) {
				if (this.CK) this.handleClick(".item a", node);
				const items = node.querySelectorAll(".item");
				for (const item of items) {
					const info = item.querySelector("a .photo-info span:not(.mleft)");
					if (!info) continue;
					const [titleNode, secondaryNode] = info.childNodes;
					const titleTxt = titleNode.textContent.trim();
					const title = DOC.create("div", { class: "title", title: titleTxt }, titleTxt);
					if (this.LM) title.classList.add("ellipsis");
					if (secondaryNode?.nodeName === "BR") {
						info.removeChild(secondaryNode);
						title.classList.add("line-4");
					}
					info.replaceChild(title, titleNode);
				}
				for (const item of items) {
					let code = item.querySelector("date");
					if (!code) continue;
					code = code.textContent.trim().toUpperCase();
					const resource = await this.fetchMatch(code);
					if (!resource?.length) continue;
					item.querySelector(".title").classList.add("matched");
					const photo = item.querySelector(".photo-frame");
					photo.classList.add("playBtn");
					photo.setAttribute("title", "点击播放");
					photo.addEventListener("click", e => {
						e.stopPropagation();
						e.preventDefault();
						GM_openInTab(`${this.pcPreview}${resource[0].pickCode}`, { active: true });
					});
				}
			},
			modifyLayout() {
				const oldWaterfall = DOC.querySelector("#waterfall");
				if (!oldWaterfall) return;
				const newWaterfall = DOC.querySelector("#waterfall #waterfall");
				if (newWaterfall) oldWaterfall.parentNode.replaceChild(newWaterfall, oldWaterfall);
				const waterfall = DOC.querySelector("#waterfall");
				waterfall.insertAdjacentHTML(
					"afterbegin",
					`<div class="item-sizer"></div><div class="gutter-sizer"></div>`
				);
				waterfall.insertAdjacentHTML(
					"afterend",
					`<div class="page-load-status"><span class="loader-ellips infinite-scroll-request">Loading...</span><span class="infinite-scroll-last">End of content</span><span class="infinite-scroll-error">No more pages to load</span></div>`
				);
				DOC.querySelector(".container-fluid .row")?.classList.add("scrollBox");
			},
			handleWaterfall() {
				const infScroll = this.waterfallLayout(
					"#waterfall",
					".item",
					{},
					{
						path: !/^\/(uncensored\/)?(search|searchstar)+\//i.test(location.pathname)
							? "#next"
							: function () {
									const { pathname } = location;
									const items = ["search", "searchstar"];
									for (const item of items) {
										if (pathname.indexOf(`${item}/`) < 0) continue;
										let [prefix, suffix] = pathname.split("&");
										suffix = suffix ?? "";
										prefix = prefix.split("/");
										let pre = "";
										for (let index = 0; index <= prefix.indexOf(item) + 1; index++) {
											pre = `${pre}${prefix[index]}/`;
										}
										return `${pre}${this.loadCount + 2}&${suffix}`;
									}
							  },
					}
				);
				infScroll.on("load", e => this.modifyItem(e));
			},
		};
		genre = {
			docStart() {
				GM_addStyle(`
                footer { display: none !important; }
                button.btn.btn-danger.btn-block.btn-genre {
                    position: fixed !important;
                    bottom: 0 !important;
                    margin: 0 !important;
                    left: 0 !important;
                    border: 0 !important;
                    border-radius: 0 !important;
                }
                `);
				if (this.DM) {
					const dStyle = this.darkStyle();
					this.darkMode(dStyle);
					const { SecondaryBackground } = dStyle;
					GM_addStyle(`.genre-box { background-color: ${SecondaryBackground} !important; }`);
				}
			},
			contentLoaded() {
				if (!DOC.querySelector("button.btn.btn-danger.btn-block.btn-genre")) return;
				const box = DOC.querySelectorAll(".genre-box");
				box[box.length - 1].setAttribute("style", "margin-bottom:65px");
			},
		};
		forum = {
			docStart() {
				GM_addStyle(`
                .bcpic, .banner728, .sd.sd_allbox > div:last-child { display: none !important; }
                .jav-button { margin-top: -3px !important; }
                #toptb {
                    position: fixed !important;
                    top: 0 !important;
                    left: 0 !important;
                    right: 0 !important;
                    z-index: 999 !important;
                }
                #wp { margin-top: 55px !important; }
                `);
			},
		};
		details = {
			docStart() {
				GM_addStyle(`
                .info .glyphicon-info-sign,
                h4[style="position:relative"],
                h4[style="position:relative"] + .row { display: none !important; }
                .info ul { margin: 0 !important; }
                .screencap { max-height: 600px; overflow: hidden; }
                #avatar-waterfall, #sample-waterfall, #related-waterfall { margin: -5px !important; }
                .photo-info { height: auto !important; }
                .bigImage { display: block; }
                `);
				// insert dom
				GM_addStyle(`
                #exp, #collapse { display: none; }
                #expBtn {
                    position: absolute;
                    top: 0;
                    height: 40px;
                    line-height: 40px;
                    left: 0;
                    right: 0;
                    cursor: pointer;
                    background: rgba(0,0,0,.7);
                    color: #fff;
                    margin: 560px 15px 0 15px;
                    z-index: 99;
                }
                #expBtn::after { content: "▼ 展开"; }
                #exp:checked + .screencap  { max-height: none; }
                #exp:checked + .screencap > #expBtn {
                    top: auto;
                    margin: 0 15px;
                    bottom: 0;
                }
                #exp:checked + .screencap > #expBtn::after { content: "▲ 收起"; }

                #collapseBtn::after { content: "▼ 展开表格"; }
                #collapse:checked + .movie { max-height: none; }
                #collapse:checked + .movie #collapseBtn::after { content: "▲ 收起表格"; }

                #resBox a { color: #CC0000 !important; }
                #smartOff { width: 100%; }
                `);
				if (this.DM) {
					const dStyle = this.darkStyle();
					this.darkMode(dStyle);
					const { SecondaryBackground, LabelColor, Grey } = dStyle;
					GM_addStyle(`
                    .movie, .sample-box, .movie-box, .photo-info { background: ${SecondaryBackground} !important; }
                    .photo-info { color: ${LabelColor} !important; }
                    .avatar-box, .avatar-box span, .info ul li, .info .star-name {
                        background: ${SecondaryBackground} !important;
                        border-color: ${Grey} !important;
                        color: ${LabelColor} !important;
                    }
                    `);
				}
			},
			contentLoaded() {
				if (this.CK) {
					this.handleClick("a.movie-box");
					this.handleClick("a.avatar-box");
				}
				// insert copy
				const handleCopy = selectors => {
					const node = DOC.querySelector(selectors);
					if (!node) return;
					const copy = node?.textContent.trim();
					if (!copy) return;
					const copyNode = DOC.create(
						"a",
						{ title: copy, "data-copy": copy, href: "", style: "margin-left:16px" },
						"复制"
					);
					copyNode.addEventListener("click", this.copyTxt);
					node.appendChild(copyNode);
				};
				handleCopy("h3");
				handleCopy("span[style='color:#CC0000;']");
				// expBtn
				const screencap = DOC.querySelector(".screencap");
				const bigImage = screencap.querySelector(".bigImage img");
				bigImage.onload = () => this.load();
				if (bigImage.complete) this.load();
				screencap.insertAdjacentHTML("beforebegin", `<input type="checkbox" id="exp">`);
				screencap.insertAdjacentHTML("beforeend", `<label for="exp" id="expBtn"></label>`);

				const info = DOC.querySelector(".col-md-3.info");
				// resource
				info.insertAdjacentHTML(
					"beforeend",
					`<p id="playerBox"><span class="header">在线播放:</span><span class="genre">查询中...</span></p>
                    <p id="resBox"><span class="header">网盘资源:</span><span class="genre">查询中...</span></p>`
				);
				// smart offLine
				const smartRes = DOC.create("button", { class: "btn btn-default", id: "smartOff" }, "一键离线");
				smartRes.addEventListener("click", e => this.handleOffLine(e, "smart"));
				info.insertAdjacentElement("beforeend", smartRes);
				// table
				DOC.querySelector("#magnet-table tbody tr").insertAdjacentHTML(
					"beforeend",
					`<td style="text-align:center;white-space:nowrap">操作</td>`
				);
				// ellipsis
				for (const item of DOC.querySelectorAll(".photo-info span")) item.classList.add("ellipsis");
				// handleFetch
				const params = this.getParams();
				if (!params) return;
				this.handleResource(params);
				this.handleStar(params);
				this.handleImage(params);
				this.handleVideo(params);
				this.handlePlayer(params);

				const magnetObs = new MutationObserver(() => this.handleMagnet(params));
				magnetObs.observe(DOC.querySelector("#movie-loading"), {
					attributes: true,
					attributeFilter: ["style"],
				});

				const tableObs = new MutationObserver(mutationsList => {
					this.modifyTable();
					mutationsList.forEach(({ addedNodes }) => {
						if (addedNodes) {
							for (const node of addedNodes) {
								if (node.nodeName === "TR") this.modifyTR(node);
								if (node.nodeName === "TBODY") {
									const trs = node?.querySelectorAll("tr");
									if (trs.length) for (const tr of trs) this.modifyTR(tr);
								}
							}
						}
					});
				});
				tableObs.observe(DOC.querySelector("#magnet-table"), { childList: true });
			},
			load() {
				const maxHei = this.maxHei;
				const img = DOC.querySelector(".screencap").children[0];
				const styleHei = img.height || img.clientHeight || img.offsetHeight;
				if (styleHei > maxHei) {
					GM_addStyle(`.screencap { max-height: ${maxHei}px; } #expBtn { margin-top: ${maxHei - 40}px; }`);
				} else {
					GM_addStyle(
						`.screencap { max-height: ${styleHei + 40}px; } #expBtn { margin-top: ${styleHei}px; }`
					);
				}
			},
			getParams() {
				// regex
				const charReg = /[\u4e00-\u9fa5:]/g;
				const studioReg = /製作商:/g;
				const starReg = /暫無出演者資訊/g;
				// dom
				const info = DOC.querySelector(".info");
				const infos = info.querySelectorAll("p");
				// params
				let [code, date] = infos;
				if (!code || !date) return;
				code = code.textContent.replace(charReg, "").trim().toUpperCase();
				date = date.textContent.replace(charReg, "").trim().toUpperCase();
				let studio = "";
				for (const { textContent: text } of infos) {
					if (!studioReg.test(text)) continue;
					studio = text.replace(studioReg, "").trim();
					break;
				}
				return {
					code,
					date,
					studio,
					star: !starReg.test(info.textContent),
					title: DOC.querySelector("h3").textContent.replace("复制", "").trim(),
					res: getItem(code),
				};
			},
			modifyTR(node) {
				const href = node?.querySelector("td a")?.href;
				if (!href) return;
				const td = DOC.create("td", { style: "text-align:center;white-space:nowrap" });
				const copy = DOC.create(
					"a",
					{ href, "data-copy": href, title: "复制磁力链接", style: "margin-right:16px" },
					"复制"
				);
				const offline = DOC.create("a", { href, title: "仅添加离线任务" }, "离线下载");
				copy.addEventListener("click", this.copyTxt);
				offline.addEventListener("click", e => this.handleOffLine(e, href));
				td.appendChild(copy);
				td.appendChild(offline);
				node.appendChild(td);
			},
			modifyTable() {
				if (DOC.querySelector("#collapse")) return;
				const trs = DOC.querySelectorAll("#magnet-table tr");
				const limit = 7;
				if (trs?.length <= limit) return;
				let maxHeight = 12;
				for (let index = 0; index < limit; index++) {
					const tr = trs[index];
					maxHeight += (tr.height || tr.clientHeight || tr.offsetHeight) + 1;
					if (index) continue;
					const td = tr.querySelector("td:last-child");
					td.textContent = "";
					td.insertAdjacentHTML(
						"beforeend",
						`<label for="collapse" id="collapseBtn" class="btn btn-mini-new btn-primary" title="点击展开/收起表格"></label>`
					);
				}
				DOC.querySelector("#magneturlpost + .movie").insertAdjacentHTML(
					"beforebegin",
					`<input type="checkbox" id="collapse">`
				);
				GM_addStyle(`#collapse + .movie { max-height: ${maxHeight - 1}px; }`);
			},
			async handleResource({ code }) {
				const resource = await this.fetchResource(code);
				upItem(code, { resource });
				const resBox = DOC.querySelector("#resBox");
				for (const old of resBox.querySelectorAll(".genre")) resBox.removeChild(old);
				if (!resource?.length) return resBox.insertAdjacentHTML("beforeend", `<span class="genre">无</span>`);
				resBox.insertAdjacentHTML(
					"beforeend",
					resource.reduce(
						(acc, { name, pickCode, date }) =>
							`${acc}
                            <span class="genre">
                                <a href="${this.pcPreview}${pickCode}" title="${date}/${name}" target="_blank">
                                    ${name.length > 20 ? `${name.substr(0, 20)}...` : name}
                                </a>
                            </span>`,
						""
					)
				);
				return resource;
			},
			async handleStar({ code, star, res }) {
				if (star) return;
				star = res?.star ?? [];
				if (!star?.length) {
					star = await this.fetchStar(code);
					if (!star?.length) return;
					upItem(code, { star });
				}
				const p = DOC.create("p");
				p.insertAdjacentHTML(
					"beforeend",
					star.reduce(
						(acc, cur) => `${acc}<span class="genre"><a href="/search/${cur}">${cur}</a></span>`,
						""
					)
				);
				DOC.querySelector(".info").replaceChild(
					p,
					DOC.querySelector("span.glyphicon.glyphicon-info-sign.mb20")?.nextSibling
				);
			},
			async handleImage({ code, res, date }) {
				let image = res?.image ?? "";
				if (!image) {
					image = await this.fetchImage(code, date);
					if (!image) return;
					upItem(code, { image });
				}
				const screencap = DOC.querySelector(".screencap");
				const placeholder = DOC.create("img", { src: GM_getResourceURL("loading") });
				screencap.appendChild(placeholder);
				const img = DOC.create("img", { src: image, title: "点击收起", style: "cursor:pointer" });
				img.addEventListener("click", () => {
					if (getScrollTop() >= this.maxHei) window.scrollTo(0, 0);
					DOC.querySelector("#exp").checked = false;
				});
				img.onload = () => screencap.replaceChild(img, placeholder);
			},
			async handleVideo({ code, studio, res }) {
				let video = res?.video ?? "";
				if (!video) {
					video = await this.fetchVideo({ code, studio });
					if (!video) return;
					upItem(code, { video });
				}

				const title = "预览视频";
				const playVideo = e => {
					e.preventDefault();
					e.stopPropagation();
					DOC.querySelector("#video-mask").setAttribute("style", "display:flex");
					const video = DOC.querySelector("video");
					video.play();
					video.focus();
					DOC.onkeydown = event => {
						const e = event || window.event;
						if (e && e.keyCode === 27) pauseVideo();
					};
				};
				const pauseVideo = () => {
					DOC.querySelector("#video-mask").setAttribute("style", "display:none");
					DOC.querySelector("video").pause();
					DOC.onkeydown = null;
				};

				const videoNode = DOC.create("video", { controls: "controls", src: video, width: 720 });
				videoNode.currentTime = 3;
				videoNode.preload = "auto";
				videoNode.muted = true;
				const closeBtn = DOC.create(
					"button",
					{ title: "Close (Esc)", type: "button", class: "mfp-close" },
					"×"
				);
				closeBtn.addEventListener("click", pauseVideo);
				const mask = DOC.create("div", { id: "video-mask", class: "mask" });
				mask.appendChild(closeBtn);
				mask.appendChild(videoNode);
				DOC.body.appendChild(mask);

				const bigImage = DOC.querySelector(".bigImage");
				const bImg = bigImage.querySelector("img");
				const playBtn = DOC.create("div", { class: "playBtn", title });
				playBtn.addEventListener("click", playVideo);
				playBtn.appendChild(bImg);
				bigImage.appendChild(playBtn);

				const thumb = bImg.src;
				const box = DOC.create("a", { class: "sample-box", href: thumb, title });
				box.addEventListener("click", playVideo);
				box.insertAdjacentHTML("beforeend", `<div class="photo-frame playBtn"><img src="${thumb}"></div>`);

				let waterfall = DOC.querySelector("#sample-waterfall");
				if (!waterfall) {
					DOC.querySelector("div.clearfix").insertAdjacentHTML(
						"beforebegin",
						`<div id="sample-waterfall"></div>`
					);
					waterfall = DOC.querySelector("#sample-waterfall");
					waterfall.insertAdjacentHTML("beforebegin", `<h4>樣品圖像</h4>`);
					return waterfall.appendChild(box);
				}
				const ref = waterfall.querySelector("a");
				waterfall.insertBefore(box, ref);
				const imgBtn = DOC.create(
					"button",
					{ title: "樣品圖像", type: "button", class: "mfp-close", style: "right:44px" },
					"📷"
				);
				imgBtn.addEventListener("click", () => {
					pauseVideo();
					ref.click();
				});
				mask.appendChild(imgBtn);
			},
			async handlePlayer({ code, res }) {
				let player = res?.player ?? [];
				if (!player?.length) {
					player = await this.fetchPlayer(code);
					if (player?.length) upItem(code, { player });
				}
				const box = DOC.querySelector("#playerBox");
				const genre = box.querySelector(".genre");
				if (!player?.length) return (genre.textContent = "无");
				genre.remove();
				box.insertAdjacentHTML(
					"beforeend",
					player.reduce(
						(pre, { zh, link, name, from }) => `
                        ${pre}
                        <a
                            class="btn btn-mini-new ${zh ? "btn-warning" : "btn-primary"}"
                            href="${link}"
                            target="_blank"
                            title="${name}"
                        >
                            <strong>▶ ${from}</strong>
                        </a>
                        `,
						""
					)
				);
			},
			async handleMagnet({ code, res }) {
				let magnet = res?.magnet ?? [];
				if (!magnet?.length) {
					magnet = await this.fetchMagnet(code);
					if (!magnet?.length) return;
					upItem(code, { magnet });
				}
				const tdStyle = "text-align:center;white-space:nowrap";
				magnet = magnet.reduce(
					(pre, { link, name, styleClass, href, from, zh, size, date }) => `
                    ${pre}
                    <tr>
                        <td>
                            <a href="${link}">${name}</a>
                            <a class="btn btn-mini-new ${styleClass}" href="${href}" target="_blank" title="磁链详情">
                                <strong>★ ${from}</strong>
                            </a>
                            ${zh ? `<a class="btn btn-mini-new btn-warning disabled">字幕</a>` : ""}
                        </td>
                        <td style="${tdStyle}">${size}</td>
                        <td style="${tdStyle}">${date}</td>
                    </tr>
                    `,
					`<tr style="color:#CC0000"><td colspan="4">下方磁力链接来自外部</td></tr>`
				);
				DOC.querySelector("#magnet-table").insertAdjacentHTML("beforeend", magnet);
			},
			async handleOffLine(e, link) {
				e.preventDefault();
				e.stopPropagation();
				if (!link) return;
				const node = e.target;
				const text = node?.textContent ?? "";
				node.textContent = "请求中...";
				node.setAttribute("disabled", true);
				node.setAttribute("style", "pointer-events:none");

				const clickUrl = "http://115.com/?tab=offline&mode=wangpan";
				if (link !== "smart") {
					let res = await this.fetchOffLine(link);
					const { state = false, error_msg = "" } = res;
					notify({
						title: `请求${state ? "成功" : "失败"}`,
						text: `${error_msg ? error_msg : "仅添加离线任务"}`,
						image: `${state ? "info" : "fail"}`,
						clickUrl,
					});
				} else {
					const params = this.getParams();
					let links = [];
					const trs = DOC.querySelectorAll("#magnet-table tr");
					for (let index = 1; index < trs.length; index++) {
						let [link, size, date] = trs[index]?.querySelectorAll("td");
						if (!link || !size || !date) continue;
						links.push({
							link: link.querySelector("a").href.split("&")[0],
							zh: !!link.querySelector("a.btn.btn-mini-new.btn-warning.disabled"),
							size: transToBytes(size.textContent.trim()),
							date: date.textContent.trim(),
						});
					}
					links = unique(links, "link");
					if (links.length > 1) {
						links.sort((pre, next) => {
							if (pre.zh === next.zh) {
								if (pre.size === next.size) return next.date - pre.date;
								return next.size - pre.size;
							} else {
								return pre.zh > next.zh ? -1 : 1;
							}
						});
					}
					for (let index = 0; index < links.length; index++) {
						const item = links[index];
						let res = await this.fetchOffLine(item.link);
						if (!res || res?.errcode === 911) break;
						if (res?.state) {
							res = await this.checkResource(params);
							if (res) {
								this.afterAction({ zh: item.zh, ...params, res });
								notify({
									title: "任务成功",
									text: "进行后续操作",
									image: "success",
									clickUrl,
								});
								break;
							}
						}
						if (index !== links.length - 1) continue;
						notify({ title: "任务失败", image: "fail", clickUrl });
					}
				}

				node.textContent = text;
				node.removeAttribute("disabled");
				node.removeAttribute("style");
			},
			async checkResource({ code }) {
				const old = getItem(code)?.resource ?? [];
				await delay(2500);
				const res = await this.handleResource({ code });
				if (Array.isArray(res) && res?.length) {
					const today = res.filter(({ date }) => date === getDate());
					if (today.length && res.length > old.length) return today;
				}
				return false;
			},
			async afterAction({ zh, res, title, code }) {
				res = res.filter(({ name }) => name.indexOf(title) === -1);
				if (!res.length) return;
				title = `${zh ? "【中文字幕】" : ""}${title}`;
				res = res.map(item => {
					const suffix = item.name.split(".").pop().toLowerCase();
					return { ...item, file_name: `${title}.${suffix}` };
				});
				await this.fetchRename(res);
				if (this.CID) {
					const moveRes = await this.fetchMove(res);
					if (moveRes?.state) this.fetchDelDir(res);
				}
				this.handleResource({ code });
			},
		};
	}
	class JavDB extends Common {
		constructor() {
			super();
			this.start();
			const tag = Object.keys(this.routeReg).find(key => this.routeReg[key].test(location.pathname));
			return { ...this, ...this[tag] };
		}
		routeReg = {
			waterfall:
				/^\/$|^\/(censored|uncensored|western|fc2|anime|tags|search|video_codes|rankings|series|makers)/i,
			actors: /^\/actors/i,
			details: /^\/v\//i,
		};
		start = () => {
			this.registerMenu(this.menus.filter(item => item.key !== "DM"));
			this.initDB();
			this.customStyle();
			this.customMode();
			DOC.addEventListener("DOMContentLoaded", () => {
				DOC.addEventListener("keyup", event => {
					const e = event || window.event;
					if (e && e.keyCode === 191 && !["INPUT", "TEXTAREA"].includes(DOC.activeElement.nodeName)) {
						DOC.querySelector("#video-search").focus();
					}
				});
			});
		};
		customMode = () => {
			GM_addStyle(`.message-body .moj-content { display: none !important; }`);
		};
		waterfall = {
			docStart() {},
			contentLoaded() {
				this.modifyItem();
			},
			load() {},
			async modifyItem(node = DOC) {
				if (this.CK) this.handleClick(".column a", node);
				const items = node.querySelectorAll(".column");
				for (const item of items) {
					let code = item.querySelector(".uid");
					if (!code) continue;
					code = code.textContent.trim().toUpperCase();
					const resource = await this.fetchMatch(code);
					if (!resource?.length) continue;
					item.querySelector(".video-title").classList.add("matched");
					const photo = item.querySelector(".item-image");
					photo.classList.add("playBtn");
					photo.setAttribute("title", "点击播放");
					photo.addEventListener("click", e => {
						e.stopPropagation();
						e.preventDefault();
						GM_openInTab(`${this.pcPreview}${resource[0].pickCode}`, { active: true });
					});
				}
			},
		};
		actors = {
			docStart() {},
			contentLoaded() {
				if (this.CK) this.handleClick(".actor-box a");
			},
			load() {},
		};
		details = {
			docStart() {},
			contentLoaded() {},
			load() {},
		};
	}

	let matched = MatchDomains.find(({ regex }) => regex.test(location.host))?.domain;
	if (matched) matched = eval(`new ${matched}();`);
	matched?.docStart();
	DOC.addEventListener("DOMContentLoaded", () => matched?.contentLoaded());
	window.addEventListener("load", () => matched?.load());

	if (/captchaapi\.115\.com/g.test(location.host)) {
		DOC.addEventListener("DOMContentLoaded", () => {
			window.focus();
			const btn = DOC.querySelector("#js_ver_code_box button[rel='verify']");
			btn.addEventListener("click", () => {
				const interval = setInterval(() => {
					if (DOC.querySelector("div[rel='error_box']").getAttribute("style").indexOf("none") !== -1) {
						window.open("", "_self");
						window.close();
					}
				}, 300);
				setTimeout(() => clearInterval(interval), 600);
			});
		});
	}
})();