JavScript

暗黑模式、滚动加载、标题机翻、搜索快捷键、在线播放、磁力搜索、115资源匹配 & 离线下载、预览视频、预览大图、演员匹配...

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

// ==UserScript==
// @name            JavScript
// @namespace       https://greasyfork.org/users/175514
// @description     暗黑模式、滚动加载、标题机翻、搜索快捷键、在线播放、磁力搜索、115资源匹配 & 离线下载、预览视频、预览大图、演员匹配...
// @version         2.5.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: /javdb[\d]*\.com/gi,
		},
	];
	// 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);
	};
	const tooltip = msg => {
		console.log(
			`%c ${msg} %c By ${GM_info.script.name} v${GM_info.script.version} %c`,
			"background:#1890FF;padding:1px;border-radius:3px 0 0 3px;color:#fff",
			"background:#555555;padding:1px;border-radius:0 3px 3px 0;color:#fff",
			"background:transparent"
		);
	};

	class Common {
		docStart = () => {};
		contentLoaded = () => {};
		load = () => {};
		mPoint = 0;
		pcPreview = "https://v.anxia.com/?pickcode=";
		clickUrl = "http://115.com/?tab=offline&mode=wangpan";
		expHei = 600;
		expBtnHei = 40;
		menus = [
			{ title: "点击事件", key: "CK", command: "c", defaultVal: true },
			{ title: "暗黑模式", key: "DM", command: "d", defaultVal: true },
			{ title: "滚动加载", key: "LM", command: "s", defaultVal: true },
			{
				title: "离线后操作目录CID",
				key: "CID",
				command: "a",
				cb: uniKey => {
					const val = prompt("用于离线下载后移动,删除操作", GM_getValue(uniKey) ?? "");
					if (!val) return;
					GM_setValue(uniKey, val);
					location.reload();
				},
				defaultVal: "",
			},
		];
		registerMenu = (menus = this.menus) => {
			for (const menu of menus) {
				const { title, key, command, defaultVal } = menu;
				const uniKey = `${this.constructor.name}_${key}`;
				const val = GM_getValue(`${uniKey}`) ?? defaultVal;
				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 {
                font-weight: bold;
                color: rgb(10,132,255) !important;
            }
            .hidden { display: none !important; }
            `);
		};
		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,
				history: false,
				historyTitle: false,
				hideNav: ".pagination",
				debug: false,
				...iParams,
			});
			return infScroll;
		};
		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);
		};
		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);
		};
		fetchTranslate = async sentence => {
			const data = {
				async: `translate,sl:auto,tl:zh-CN,st:${encodeURIComponent(
					sentence.trim()
				)},id:1642125176704,qc:true,ac:false,_id:tw-async-translate,_pms:s,_fmt:pc`,
			};
			sentence = await request(
				"https://www.google.com/async/translate?vet=12ahUKEwi03Jv2kLD1AhWRI0QIHe_TDKAQqDh6BAgCECY..i&ei=ZtfgYbSRO5HHkPIP76ezgAo&yv=3",
				data,
				"POST",
				{ headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" } }
			);
			return sentence?.querySelector("#tw-answ-target-text").textContent ?? "";
		};
		// search prefix match list
		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;
		};
		// 115 files search
		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 };
			});
		};
		// 115 files search result match
		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: "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: "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: "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",
			});
		};
		searchOnKey = selectors => {
			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(selectors).focus();
					}
				});
			});
		};
		handlePreviewFold = selectors => {
			GM_addStyle(`
            ${selectors} {
                overflow: hidden;
                height: fit-content;
                max-height: ${this.expHei}px;
            }
            #expBtn {
                top: 0;
                left: 0;
                right: 0;
                color: #fff;
                z-index: 99;
                cursor: pointer;
                position: absolute;
                background: rgba(0,0,0,.7);
                height: ${this.expBtnHei}px;
                line-height: ${this.expBtnHei}px;
                margin-top: ${this.expHei - this.expBtnHei}px;
            }
            #expBtn::after { content: "▼ 展开"; }
            #exp:checked + ${selectors} > #expBtn::after { content: "▲ 收起"; }
            #exp:checked + ${selectors}  { max-height: none; }
            #exp:checked + ${selectors} > #expBtn {
                top: auto;
                bottom: 0;
                margin-bottom: 0;
            }
            `);
			const modifyMaxHei = () => {
				const expHei = this.expHei;
				const img = DOC.querySelector(`${selectors} img`);
				const styleHei = img.height || img.clientHeight || img.offsetHeight;
				if (styleHei > expHei) {
					GM_addStyle(`
                    ${selectors} { max-height: ${expHei}px; }
                    #expBtn { margin-top: ${expHei - this.expBtnHei}px; }
                    `);
				} else {
					GM_addStyle(`
                    ${selectors} { max-height: ${styleHei + this.expBtnHei}px; }
                    #expBtn { margin-top: ${styleHei}px; }
                    `);
				}
			};
			const box = DOC.querySelector(selectors);
			const img = box.querySelector("img");
			if (img.complete) modifyMaxHei();
			img.onload = () => modifyMaxHei();
			box.insertAdjacentHTML("beforebegin", `<input type="checkbox" id="exp" class="hidden">`);
			box.insertAdjacentHTML("beforeend", `<label for="exp" id="expBtn"></label>`);
		};
	}
	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] };
		}
		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();
			this.searchOnKey("#search-input");
		};
		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 - 51px);
                        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);
					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}`;
									}
							  },
						elementScroll: ".scrollBox",
						status: ".page-load-status",
					}
				);
				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; }
				#avatar-waterfall, #sample-waterfall, #related-waterfall { margin: -5px !important; }
				.photo-info { height: auto !important; }
				#resBox a { color: #CC0000 !important; }
				#smartOff { width: 100%; }
                #expBtn { margin-left: 15px; margin-right: 15px; }
				`);
				GM_addStyle(`
                #collapseBtn::after { content: "▼ 展开表格"; }
                #collapse:checked + .movie { max-height: none; }
                #collapse:checked + .movie #collapseBtn::after { content: "▲ 收起表格"; }
                `);
				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
				this.handleCopy("h3");
				this.handleCopy("span[style='color:#CC0000;']");
				// expBtn
				this.handlePreviewFold(".screencap");

				const info = DOC.querySelector(".info");
				// resource, player
				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.handleTransTitle(params);
				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 });
			},
			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.offsetHeight;
					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" class="hidden">`
				);
				GM_addStyle(`#collapse + .movie { max-height: ${maxHeight}px; }`);
			},
			async handleTransTitle({ code, title, res }) {
				let transTitle = res?.transTitle ?? "";
				if (!transTitle) {
					transTitle = await this.fetchTranslate(title);
					if (!transTitle) return;
					upItem(code, { transTitle });
				}
				DOC.querySelector("h3").setAttribute("title", transTitle);
			},
			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.expHei) 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 });
				}
				// func
				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;
				};
				// mask
				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);
				// screencap
				const bigImage = DOC.querySelector(".bigImage");
				const playBtn = DOC.create("div", { class: "playBtn", title });
				const bImg = bigImage.querySelector("img");
				playBtn.addEventListener("click", playVideo);
				playBtn.appendChild(bImg);
				bigImage.appendChild(playBtn);
				// sample-waterfall
				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);
				}
				// jump to preview
				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}
                        <button
                            class="btn btn-mini-new ${zh ? "btn-warning" : "btn-primary"}"
                            data-link="${link}"
                            title="${name}"
                        >
                            <strong>▶ ${from}</strong>
                        </button>
                        `,
						""
					)
				);
				for (const btn of DOC.querySelectorAll("#playerBox button")) {
					btn.addEventListener("click", () => GM_openInTab(btn.dataset.link, { active: true }));
				}
			},
			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 btn-${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 = this.clickUrl;
				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 {
					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(),
						});
					}
					if (links.length) {
						if (links.length > 1) 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;
								}
							});
						}
						tooltip("磁链列表");
						console.table(links);
						const params = this.getParams();
						for (let index = 0; index < links.length; index++) {
							const { link, zh } = links[index];
							let res = await this.fetchOffLine(link);
							if (!res || res?.errcode === 911) break;
							if (res?.state) {
								res = await this.checkResource(params);
								if (res?.length) {
									this.afterAction({ ...params, zh, 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|search|video_codes|tags|rankings|actors|series|makers|directors)/i,
			details: /^\/v\//i,
		};
		start = () => {
			this.registerMenu(this.menus.filter(item => !["DM"].includes(item.key)));
			this.initDB();
			this.customStyle();
			this.customMode();
			this.searchOnKey("#video-search");
		};
		customMode = () => {
			GM_addStyle(`.message-body .moj-content { display: none !important; }`);
		};
		waterfall = {
			container: {},
			containerList: [
				{
					selectors: "#videos .columns",
					itemSelectors: "#videos .columns .column",
					item: "#videos .columns .column a",
					itemSizer: "160px",
				},
				{
					selectors: "#actors",
					itemSelectors: "#actors .box",
					item: "#actors .box",
					itemSizer: "130px",
				},
			],
			docStart() {
				GM_addStyle(`
                #tags { display: none; }
                #expBtn::after { content: "展开"; }
                #exp:checked + .tabs.is-boxed + #tags { display: block; }
                #exp:checked + .tabs.is-boxed > #expBtn::after { content: "收起"; }
                `);
				if (this.LM) {
					const gutterSizer = `20px`;
					GM_addStyle(`
                    nav.pagination, nav#footer { display: none; }
                    .gutter-sizer { width: ${gutterSizer} !important; }
                    #videos .columns .column img {
                        height: 200px !important;
                        object-fit: cover;
                    }
                    ${this.containerList.map(item => item.selectors).join(", ")} { opacity: 0; }
                    ${this.containerList.reduce(
						(pre, { itemSelectors, item, itemSizer }) => `
                        ${pre}
                        ${itemSelectors} {
                            padding: 0 !important;
                            margin: 0 0 ${gutterSizer} 0 !important;
                            width: ${itemSizer} !important;
                        }
                        ${item} { width: ${itemSizer} !important; }
                        `,
						""
					)}
                    `);
				}
			},
			contentLoaded() {
				this.modifyItem();

				if (DOC.querySelector("#tags")) {
					const tabs = DOC.querySelector(".tabs.is-boxed");
					tabs.insertAdjacentHTML("beforebegin", `<input type="checkbox" id="exp" class="hidden">`);
					tabs.insertAdjacentHTML(
						"beforeend",
						`<label class="button is-link" for="exp" id="expBtn"></label>`
					);
				}

				if (!this.LM) return;
				this.container = this.containerList.find(({ selectors }) => DOC.querySelector(selectors));
				if (location.pathname === "/rankings/fanza_award" || !this.container) {
					return GM_addStyle(`nav.pagination, nav#footer { display: flex; }`);
				}
				GM_addStyle(`
                .item-sizer { width: ${this.container.itemSizer} !important; }
                ${this.container.selectors} { margin: 0 auto !important; }
                `);
				this.modifyLayout();
				this.handleWaterfall();
			},
			async modifyItem(node = DOC) {
				if (this.CK) {
					const extra = [
						{ itemSelectors: "#series .columns .column" },
						{ itemSelectors: "#makers .columns .column" },
						{ itemSelectors: "#directors .columns .column" },
					];
					for (const { itemSelectors } of unique([...this.containerList, ...extra], "itemSelectors")) {
						this.handleClick(`${itemSelectors} a`, node);
					}
				}
				const items = node.querySelectorAll("#videos .columns .column");
				for (const item of items) {
					let img = item.querySelector("img");
					if (img) img.src = img.dataset.src;
				}
				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;
					const title = item.querySelector(".video-title");
					const titleTxt = title.textContent;
					title.textContent = "";
					title.insertAdjacentHTML("beforeend", `<span class="matched">${titleTxt}</span>`);
					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 });
					});
				}
			},
			modifyLayout() {
				const waterfall = DOC.querySelector(this.container.selectors);
				waterfall.classList.remove("grid");
				waterfall.insertAdjacentHTML(
					"afterbegin",
					`<div class="item-sizer"></div><div class="gutter-sizer"></div>`
				);
			},
			handleWaterfall() {
				const infScroll = this.waterfallLayout(
					this.container.selectors,
					this.container.itemSelectors,
					{},
					{ path: ".pagination-next" }
				);
				infScroll?.on("load", e => this.modifyItem(e));
			},
		};
		details = {
			docStart() {
				GM_addStyle(`
				.video-meta-panel {
				    padding-left: 0;
				    padding-right: 0;
				}
				.video-meta-panel > .columns {
				    margin-left: 0;
				    margin-right: 0;
				}
				.video-meta-panel > .columns > .column {
				    padding: 0;
				    margin: .75rem;
				}
				.column-video-cover .cover-container::after { height: 100%; }
                .column-video-cover img {
                    width: 100%;
                    max-height: none;
                }
				.movie-panel-info div.panel-block { padding: 8px 0; }
                #smartOff { flex: 1; }
				#resBox .button,
				#playerBox .button {
				    height: auto;
				    padding: 3px 6px;
				    line-height: unset;
				}
                #playerBox { border-bottom: 0; }
				`);
				GM_addStyle(`
                #magnets-content { overflow: hidden; }
                #collapseBtn::after { content: "▼ 展开表格" }
                #collapse:checked + .message-body > #magnets-content { max-height: none; }
                #collapse:checked + .message-body > #collapseBtn::after { content: "▲ 收起表格" }
                `);
			},
			contentLoaded() {
				if (this.CK) this.handleClick(".tile-small a.tile-item");
				// insert copy
				this.handleCopy("h2");

				this.handlePreviewFold(".column-video-cover");
				this.modifyTable();

				const info = DOC.querySelector("nav.movie-panel-info");
				// insert smartOff
				const smartRes = DOC.create(
					"a",
					{ class: "button is-info", id: "smartOff", href: "javascript:;" },
					"一键离线"
				);
				smartRes.addEventListener("click", e => this.handleOffLine(e, "smart"));
				const infos = info.querySelectorAll(".panel-block");
				const lastPanel = infos[infos.length - 1];
				lastPanel.querySelector(".columns").setAttribute("style", "flex:1");
				lastPanel.querySelector(".buttons").appendChild(smartRes);
				// insert player & resource
				info.insertAdjacentHTML(
					"beforeend",
					`<div class="panel-block" id="resBox">
				        <strong>网盘资源:</strong><span class="value">查询中...</span>
				    </div>
				    <div class="panel-block" id="playerBox">
				        <strong>在线播放:</strong><span class="value">查询中...</span>
				    </div>`
				);
				// handleFetch
				const params = this.getParams();
				if (!params) return;
				this.handleTransTitle(params);
				this.handleResource(params);
				this.handleImage(params);
				this.handleVideo(params);
				this.handlePlayer(params);
				this.handleMagnet(params);
			},
			getParams() {
				const charReg = /[\u4e00-\u9fa5:]/g;
				const studioReg = /片商:/g;
				const infos = DOC.querySelectorAll("nav.movie-panel-info .panel-block");
				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,
					video: !!DOC.querySelector(".preview-video-container"),
					title: DOC.querySelector("h2").textContent.replace("复制", "").trim(),
					res: getItem(code),
				};
			},
			modifyTable() {
				const table = DOC.querySelector("#magnets-content table");
				if (!table) return;
				const trs = table.querySelectorAll("tr");
				for (const tr of trs) {
					const href = tr.querySelector(".magnet-name a").href;
					if (!href || tr.querySelector(".offline")) continue;
					const td = DOC.create("td", { class: "sub-column offline", width: "94" });
					const btn = DOC.create(
						"button",
						{ class: "button is-info is-small", type: "button", title: "仅添加离线任务" },
						"离线下载"
					);
					btn.addEventListener("click", e => this.handleOffLine(e, href));
					td.appendChild(btn);
					tr.appendChild(td);
				}

				const limit = 6;
				if (trs?.length <= limit) return;
				let maxHeight = 0;
				for (let index = 0; index < limit; index++) maxHeight += trs[index].offsetHeight;
				const body = DOC.querySelector("#magnet-links .message-body");
				body.insertAdjacentHTML("beforebegin", `<input type="checkbox" id="collapse" class="hidden">`);
				body.querySelector(".moj-content").insertAdjacentHTML(
					"beforebegin",
					`<label for="collapse" id="collapseBtn" class="button is-info is-small mb-2"></label>`
				);
				GM_addStyle(`#magnets-content { max-height: ${maxHeight}px; }`);
			},
			async handleTransTitle({ code, title, res }) {
				let transTitle = res?.transTitle ?? "";
				if (!transTitle) {
					transTitle = await this.fetchTranslate(title);
					if (!transTitle) return;
					upItem(code, { transTitle });
				}
				DOC.querySelector("h2").setAttribute("title", transTitle);
			},
			async handleResource({ code }) {
				const resource = await this.fetchResource(code);
				upItem(code, { resource });
				const resBox = DOC.querySelector("#resBox");
				resBox.querySelector(".value")?.remove();
				resBox.querySelector(".columns")?.remove();
				if (!resource?.length) return resBox.insertAdjacentHTML("beforeend", `<span class="value">无</span>`);
				resBox.insertAdjacentHTML(
					"beforeend",
					`
                    <div class="columns">
                    <div class="column">
                    <div class="buttons are-small review-buttons">
                    ${resource.reduce(
						(acc, { name, pickCode, date }) => `
				        ${acc}
				        <a
				            class="button is-small is-danger"
				            href="${this.pcPreview}${pickCode}"
				            title="${date}/${name}"
				            target="_blank"
				        >
				        ${name.length > 20 ? `${name.substr(0, 20)}...` : name}
				        </a>
				        `,
						""
					)}
                    </div>
                    </div>
                    </div>
                    `
				);
				return resource;
			},
			async handleImage({ code, res, date }) {
				let image = res?.image ?? "";
				if (!image) {
					image = await this.fetchImage(code, date);
					if (!image) return;
					upItem(code, { image });
				}
				const cover = DOC.querySelector(".column-video-cover");
				const placeholder = DOC.create("img", { src: GM_getResourceURL("loading") });
				cover.appendChild(placeholder);
				const img = DOC.create("img", { src: image, title: "点击收起", style: "cursor:pointer" });
				img.addEventListener("click", () => {
					if (getScrollTop() >= this.expHei) window.scrollTo(0, 0);
					DOC.querySelector("#exp").checked = false;
				});
				img.onload = () => cover.replaceChild(img, placeholder);
			},
			async handleVideo({ video, code, studio, res }) {
				if (video) {
					video = DOC.querySelector("#preview-video source").src;
				} else {
					video = res?.video ?? "";
					if (!video) video = await this.fetchVideo({ code, studio });
				}
				if (!video) return;
				upItem(code, { video });

				let preview = DOC.querySelector(".tile-images.preview-images");
				if (!preview) {
					DOC.querySelector(".video-meta-panel").insertAdjacentHTML(
						"afterend",
						`<div class="columns">
                        <div class="column">
                        <article class="message video-panel">
                        <div class="message-body">
                        <div class="tile-images preview-images">
                        </div>
                        </div>
                        </article>
                        </div>
                        </div>`
					);
					preview = DOC.querySelector(".tile-images.preview-images");
				}
				if (!preview.querySelector("#preview-video")) {
					preview.insertAdjacentHTML(
						"afterbegin",
						`<a class="preview-video-container" data-fancybox="gallery" href="#preview-video">
                            <span>預告片</span>
                            <img
                                src="${DOC.querySelector(".column-video-cover img").src}"
                                class="video-cover"
                                style="width:150px;height:auto"
                            />
                        </a>
                        <video
                            id="preview-video"
                            playsinline=""
                            controls=""
                            muted=""
                            preload="auto"
                            style="display:none"
                        >
                            <source src="${video}"/>
                        </video>`
					);
				}

				const playBtn = DOC.create("a", { class: "play-button" });
				playBtn.addEventListener("click", e => {
					e.stopPropagation();
					e.preventDefault();
					DOC.querySelector(".preview-video-container").click();
				});
				playBtn.insertAdjacentHTML(
					"beforeend",
					`<span class="icon"><img src="/packs/media/images/btn-play-b414746c.svg"></span>
                    <span class="text">播放预览</span>`
				);

				let cover = DOC.querySelector(".column-video-cover .cover-container");
				if (!cover) {
					const gallery = DOC.querySelector(".column-video-cover a");
					gallery.removeAttribute("data-fancybox");
					gallery.classList.add("cover-container");
					cover = DOC.querySelector(".column-video-cover .cover-container");
					playBtn.setAttribute("style", "z-index:9");
				} else {
					playBtn.setAttribute("style", "margin-top:60px;z-index:9");
				}
				cover.insertAdjacentElement("beforeend", playBtn);
			},
			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");
				box.querySelector(".value")?.remove();
				box.querySelector(".columns")?.remove();
				if (!player?.length) return box.insertAdjacentHTML("beforeend", `<span class="value">无</span>`);
				box.insertAdjacentHTML(
					"beforeend",
					`
                    <div class="columns">
                    <div class="column">
                    <div class="buttons are-small review-buttons">
                    ${player.reduce(
						(acc, { zh, link, name, from }) => `
				        ${acc}
				        <a
				            class="button is-small ${zh ? "is-warning" : "is-info"}"
				            href="${link}"
				            title="${name}"
				            target="_blank"
				        >
				        ▶ ${from}
				        </a>
				        `,
						""
					)}
                    </div>
                    </div>
                    </div>
                    `
				);
			},
			async handleMagnet({ code, res }) {
				let magnet = res?.magnet ?? [];
				if (!magnet?.length) {
					magnet = await this.fetchMagnet(code);
					if (!magnet?.length) return;
					upItem(code, { magnet });
				}
				magnet = magnet.reduce(
					(pre, { link, name, styleClass, href, from, zh, size, date }) => `
                    ${pre}
                    <tr>
                        <td class="magnet-name">
                            <a href="${link}">
                                <span>${name}</span>
                                <br>
                                ${zh ? `<span class="tag is-warning is-small">字幕</span>` : ""}
                                <span class="tag is-${styleClass} is-small">${from}</span>
                                <span class="meta">&nbsp;( ${size} )</span>
                            </a>
                        </td>
                        <td class="sub-column" width="80">
                            <span class="time">${date}</span>
                        </td>
                        <td class="sub-column" width="70">
                            <button
                                class="button is-info is-small copy-to-clipboard"
                                data-clipboard-text="${link}"
                                type="button"
                            >
                                複製
                            </button>
                        </td>
                    </tr>`,
					""
				);
				const magnets = DOC.querySelector("#magnets-content");
				let tbody = magnets.querySelector("table tbody");
				if (!tbody) {
					magnets.textContent = "";
					magnets.insertAdjacentHTML("beforeend", `<table width="100%"><tbody></tbody></table>`);
					tbody = magnets.querySelector("table tbody");
				}
				tbody.insertAdjacentHTML("beforeend", magnet);
				this.modifyTable();
			},
			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 = this.clickUrl;
				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 {
					let links = [];
					const trs = DOC.querySelectorAll("#magnets-content tr");
					for (const tr of trs) {
						let [info, date] = tr?.querySelectorAll("td");
						if (!info || !date) continue;
						links.push({
							link: info.querySelector("a").href.split("&")[0],
							zh: info.textContent.includes("字幕"),
							size: (() => {
								let size = info.querySelector(".meta").textContent.trim();
								size = size.split(",")[0].replace(/\(|\)/gi, "").trim();
								return transToBytes(size);
							})(),
							date: date.textContent.trim(),
						});
					}
					if (links.length) {
						if (links.length > 1) 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;
								}
							});
						}
						tooltip("磁链列表");
						console.table(links);
						const params = this.getParams();
						for (let index = 0; index < links.length; index++) {
							const { link, zh } = links[index];
							let res = await this.fetchOffLine(link);
							if (!res || res?.errcode === 911) break;
							if (res?.state) {
								res = await this.checkResource(params);
								if (res?.length) {
									this.afterAction({ ...params, zh, 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 });
			},
		};
	}

	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);
			});
		});
	}
})();