JavScript

暗黑模式、滚动加载、115资源匹配 & 离线下载、预览视频、预览大图...

Version vom 08.01.2022. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name            JavScript
// @namespace       https://greasyfork.org/users/175514
// @description     暗黑模式、滚动加载、115资源匹配 & 离线下载、预览视频、预览大图...
// @version         1.1.2
// @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        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",
			]),
		},
	];
	// 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");
					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 = document.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);
	};

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

		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",
			},
		];
		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(https://javdb.com/packs/media/images/btn-play-b414746c.svg) 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; }
            `);
		};
		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) {
				let codes = [
					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"),
				];
				codes = Array.from(new Set(codes));
				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)]);
			if (typeof jbRes === "object") return jb;
			const href = jsRes?.querySelector("#content_news li a").href;
			if (!href) return;
			jsRes = await request(href);
			jsRes = jsRes?.querySelector(".news a img[alt*='.th']").src.replace(".th", "");
			if (!jsRes || !(await request(jsRes))) return;
			return jsRes;
		};
		fetchVideo = async ({ code, studio }) => {
			code = code.toLowerCase();
			if (studio) {
				const matched = this.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") ||
				""
			);
		};
		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 = Array.from(new Set(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 { 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: "收起"; }
                #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;
                    }
                    `);
				}
				DOC.addEventListener("DOMNodeInserted", ({ target: node }) => {
					if (node?.nodeName.toLowerCase() !== "tr") return;
					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);
				});
			},
			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");
				screencap.querySelector(".bigImage img").onload = () => 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="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);
			},
			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),
				};
			},
			async handleResource({ code }) {
				// console.time("handleResource");
				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>`,
						""
					)
				);
				// console.timeEnd("handleResource");
				return resource;
			},
			async handleStar({ code, star, res }) {
				// console.time("handleStar");
				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
				);
				// console.timeEnd("handleStar");
			},
			async handleImage({ code, res, date }) {
				// console.time("handleImage");
				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);
				// console.timeEnd("handleImage");
			},
			async handleVideo({ code, studio, res }) {
				// console.time("handleVideo");
				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);
				// console.timeEnd("handleVideo");
			},
			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++) {
						const item = { link: "", zh: false, size: 0, date: 0 };
						let [name, size, date] = trs[index].querySelectorAll("td");
						if (!name || !size || !date) continue;
						item.zh = !!name.querySelector("a.btn.btn-mini-new.btn-warning.disabled");
						name = name.querySelector("a");
						item.link = name.href;
						size = size.querySelector("a").textContent.trim().replace(/gb/gi, "");
						if (/mb/gi.test(size)) size = (parseFloat(size) / 1024).toFixed(2);
						item.size = Number(size);
						item.date = date.querySelector("a").textContent.trim().replace(/-/g, "");
						links.push(item);
					}
					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 });
			},
		};
	}

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