JavBus工具

暗黑模式、滚动加载、预览视频、离线下载(115会员)...

Tính đến 11-11-2021. Xem phiên bản mới nhất.

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

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

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.

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

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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            JavBus工具
// @description     暗黑模式、滚动加载、预览视频、离线下载(115会员)...
// @version         0.1
// @icon            https://z3.ax1x.com/2021/10/15/53gMFS.png
// @include         *://*.javbus.com/*
// @include         *://captchaapi.115.com/*
// @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
// @require         https://cdn.jsdelivr.net/npm/[email protected]/dist/localforage.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
// @run-at          document-start
// @grant           GM_xmlhttpRequest
// @grant           GM_addStyle
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_notification
// @grant           GM_setClipboard
// @grant           GM_getResourceURL
// @grant           GM_openInTab
// @grant           GM_info
// @grant           GM_registerMenuCommand
// @connect         *
// @namespace https://greasyfork.org/users/175514
// ==/UserScript==

/**
 * TODO:
 * ✔️ 暗黑模式
 * ✔️ 点击事件
 * ✔️ 滚动加载
 * ✔️ 获取预览大图
 * ✔️ 获取预览视频
 * ✔️ 获取演员名单(如无)
 * ✔️ 离线下载(请求离线成功2秒后查询结果汇报)
 * ✔️ 一键离线(字幕>尺寸>日期)排队执行离线下载
 * ✔️ 115账号验证弹窗
 * ✔️ 查询115是否已有离线资源
 */
(function () {
	"use strict";

	const SUFFIX = " !important";
	const dm = GM_getValue("dm") ?? matchMedia("(prefers-color-scheme: dark)").matches;
	const lm = GM_getValue("lm") ?? false;
	const ck = GM_getValue("ck") ?? true;
	let rootId = GM_getValue("rid") ?? "";
	let mousePoint = 0;

	const { host, pathname } = location;

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

	const lf = localforage;
	lf.upItem = async (key, val) => {
		let item = (await lf.getItem(key)) ?? {};
		await lf.setItem(key, Object.assign(item, val));
	};

	// 网络请求
	const request = (url, method = "GET", data = {} | "", params = {}) => {
		if (!url) return;
		if (method === "POST") {
			params.headers = Object.assign(params.headers ?? {}, {
				"Content-Type": "application/x-www-form-urlencoded",
			});
		}
		if (typeof data === "object" && Object.keys(data).length) {
			data = Object.keys(data).reduce(
				(pre, cur, index, arr) =>
					`${pre}=${data[pre]}&${cur}=${data[cur]}${index !== arr.length - 1 ? "&" : ""}`
			);
			data = encodeURI(data);
			if (method === "GET") url = `${url}?${data}`;
		}
		return new Promise(resolve => {
			GM_xmlhttpRequest({
				url,
				method,
				data,
				timeout: 20000,
				onload: ({ responseText }) => {
					if (/<\/?[a-z][\s\S]*>/i.test(responseText)) {
						responseText = new DOMParser().parseFromString(responseText, "text/html");
					}
					if (/^{.*}$/.test(responseText)) {
						responseText = JSON.parse(responseText);
					}
					const errcode = responseText?.errcode;
					if (`${errcode}` === "911") verify();
					resolve(responseText);
				},
				...params,
			});
		});
	};

	const notifiy = (title = "", text = "", icon = "info", clickUrl = "", params = {}) => {
		if (typeof title === "object") params = title;
		GM_notification({
			title,
			text: text || GM_info.script.name,
			image: GM_getResourceURL(params.icon || icon),
			highlight: true,
			timeout: 3000,
			onclick: () => {
				clickUrl = params.clickUrl || clickUrl;
				if (!clickUrl) return;
				GM_openInTab(clickUrl, { active: true });
			},
			...params,
		});
	};

	// 115验证账号
	const verify = async () => {
		const time = new Date().getTime();
		let h = 667;
		let w = 375;
		let t = (window.screen.availHeight - h) / 2;
		let l = (window.screen.availWidth - w) / 2;
		window.open(
			`https://captchaapi.115.com/?ac=security_code&type=web&cb=Close911_${time}`,
			"请验证账号",
			`height=${h},width=${w},top=${t},left=${l},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
		);
	};

	const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

	class Common {
		docStart = () => {};
		contentLoaded = () => {};
		load = () => {};
	}
	class Waterfall extends Common {
		isHome = /^(\/(page\/\d+)?|\/uncensored(\/page\/\d+)?)+$/i.test(pathname);
		docStart = () => {
			GM_addStyle(`
            .search-header {
                padding: 0${SUFFIX};
                background: none${SUFFIX};
                box-shadow: none${SUFFIX};
            }
            .photo-frame {
                margin: 10px${SUFFIX};
            }
            .photo-frame img {
                height: 100%${SUFFIX};
                width: 100%${SUFFIX};
                object-fit: cover${SUFFIX};
                margin: 0${SUFFIX};
            }
            .photo-info {
                padding: 10px${SUFFIX};
            }
            .alert-page {
                margin: 20px${SUFFIX};
            }
            `);
			if (!lm) return;
			const itemSizer = `167px`;
			const gutterSizer = `20px`;
			GM_addStyle(`
            .pagination,
            footer {
                display: none${SUFFIX};
            }
            .page-load-status {
                display: none;
                padding-bottom: ${gutterSizer};
                text-align: center;
            }
            body {
                overflow: hidden;
            }
            .scrollBox {
                height: calc(100vh - 50px);
                overflow: hidden;
                overflow-y: scroll;
            }
			#waterfall {
			    opacity: 0;
			    margin: ${gutterSizer} auto 0 auto${SUFFIX};
			}
			.item-sizer,
			.item a {
			    width: ${itemSizer}${SUFFIX};
			}
			.gutter-sizer {
                width: ${gutterSizer}${SUFFIX};
			}
            .item a {
                margin: 0 0 ${gutterSizer} 0${SUFFIX};
            }
			`);
		};
		contentLoaded = () => {
			const nav = doc.querySelector(".search-header .nav");
			if (nav) nav.setAttribute("class", "nav nav-pills");
			ck && this.handleClick();
			if (!lm) return;
			this.handleLoadMore();
			if (!this.isHome) this.modifyLayout();
		};
		load = () => {
			if (lm && this.isHome) this.modifyLayout();
		};
		handleClick = () => {
			const isItem = e => {
				const elem = e.target.offsetParent;
				let re = elem.nodeName.toLowerCase() === "div" && /^item/i.test(elem.className);
				if (re) re = elem.querySelector("a").href;
				return re;
			};
			doc.body.addEventListener("contextmenu", e => {
				isItem(e) && e.preventDefault();
			});
			doc.body.addEventListener("click", e => {
				const href = isItem(e);
				if (!href) return;
				e.preventDefault();
				GM_openInTab(href, { active: true });
			});
			doc.body.addEventListener("mousedown", e => {
				const href = isItem(e);
				if (!href || e.button !== 2) return;
				e.preventDefault();
				mousePoint = e.screenX + e.screenY;
			});
			doc.body.addEventListener("mouseup", e => {
				const href = isItem(e);
				const num = e.screenX + e.screenY - mousePoint;
				if (!href || e.button !== 2 || num > 5 || num < -5) return;
				e.preventDefault();
				GM_openInTab(href);
			});
		};
		handleLoadMore = () => {
			let oldWaterfall = doc.querySelector("#waterfall");
			if (!oldWaterfall) return GM_addStyle(`#waterfall { opacity: 1; }`);
			let newWaterfall = doc.querySelector("#waterfall #waterfall");
			if (newWaterfall) oldWaterfall.parentNode.replaceChild(newWaterfall, oldWaterfall);
			let itemSizer = doc.create("div", { class: "item-sizer" });
			let gutterSizer = doc.create("div", { class: "gutter-sizer" });
			let waterfall = doc.querySelector("#waterfall");
			let ref = doc.querySelector(".item");
			waterfall.insertBefore(itemSizer, ref);
			waterfall.insertBefore(gutterSizer, ref);
			doc.querySelectorAll("div.container-fluid")[1].querySelector(".row").classList.add("scrollBox");
			const status = doc.create("div", { class: "page-load-status" });
			const request = doc.create("span", { class: "loader-ellips infinite-scroll-request" }, "Loading...");
			const last = doc.create("span", { class: "infinite-scroll-last" }, "End of content");
			const error = doc.create("span", { class: "infinite-scroll-error" }, "No more pages to load");
			status.appendChild(request);
			status.appendChild(last);
			status.appendChild(error);
			doc.querySelector(".scrollBox").appendChild(status);
		};
		modifyLayout = () => {
			let waterfall = doc.querySelector("#waterfall");
			if (!waterfall) return;
			let msnry = new Masonry(waterfall, {
				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 },
			});
			imagesLoaded(waterfall, () => {
				msnry.options.itemSelector = ".item";
				let elems = waterfall.querySelectorAll(".item");
				this.modifyItem();
				msnry.appended(elems);
				GM_addStyle(`#waterfall { opacity: 1; }`);
			});
			// 搜索页滚动加载需特殊处理
			const path = !/^\/(uncensored\/)?(search|searchstar)+\//i.test(pathname)
				? "#next"
				: function () {
						const items = ["search", "searchstar"];
						for (const item of items) {
							if (pathname.indexOf(`${item}/`) < 0) continue;
							let [prefix, suffix] = pathname.split("&");
							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}`;
						}
				  };
			let infScroll = new InfiniteScroll(waterfall, {
				path,
				append: ".item",
				outlayer: msnry,
				elementScroll: ".scrollBox",
				history: false,
				historyTitle: false,
				hideNav: ".pagination",
				status: ".page-load-status",
				debug: false,
			});
			infScroll.on("load", this.modifyItem);
		};
		modifyItem = (node = doc, path) => {
			const infos = node.querySelectorAll(".item a .photo-info span:not(.mleft)");
			for (const info of infos) {
				const [titleNode, secondaryNode] = info.childNodes;
				const titleTxt = titleNode.nodeValue.trim();
				const ellipsis = doc.create("div", { class: "ellipsis", title: titleTxt }, titleTxt);
				if (secondaryNode && secondaryNode.nodeName === "BR") {
					info.removeChild(secondaryNode);
					ellipsis.classList.add("line-4");
				}
				titleNode.parentNode.replaceChild(ellipsis, titleNode);
			}
		};
	}
	class Details extends Common {
		codeKey = "";
		code = "";
		lfItem = {};
		config = async codeKey => {
			if (!codeKey) return;
			this.codeKey = codeKey;
			this.code = codeKey.split("/")[0];
			this.lfItem = (await lf.getItem(codeKey)) ?? {};
		};
		docStart = () => {
			GM_addStyle(`
            .info .glyphicon-info-sign,
            h4[style="position:relative"],
            h4[style="position:relative"] + .row {
                display: none${SUFFIX};
            }
            .info ul {
                margin: 0${SUFFIX};
            }
            .screencap {
                max-height: 600px;
                overflow: hidden; 
            }
            #avatar-waterfall,
            #sample-waterfall,
            #related-waterfall {
                margin: -5px${SUFFIX};
            }
            .screencap {
                max-height: 600px;
                overflow: hidden;
            }
            .photo-info {
                height: auto${SUFFIX};
            }
            `);
			GM_addStyle(`
            #previewVideo { position: relative; }
            #previewVideo:after {
                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 40px;
                bottom: 0;
                content: "";
                display: block;
                left: 0;
                position: absolute;
                right: 0;
                top: 0;
                height: 100%;
            }
            `);
			GM_addStyle(`
            #mask {
                position: fixed;
                width: 100%;
                height: 100%;
                z-index: 9999;
                left: 0;
                top: 0;
                background: rgba(11,11,11,.8);
                display: flex;
                justify-content: center;
                align-items: center;
                display: none;
            }
            `);
			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: "收起";
            }
            @media screen and (max-width: 480px) {
                #btnGrp {
                    margin-bottom: 15px;
                }
                .screencap {
                    max-height: 280px;
                }
                #expBtn {
                    margin-left: 0;
                    margin-right: 0;
                }
            }
            #resBox a {
                color: #CC0000${SUFFIX};
            }
            `);
			doc.addEventListener("DOMNodeInserted", e => {
				let node = e.target;
				if (node.nodeName.toLowerCase() !== "tr") return;
				let href = node.querySelector("td a")?.href;
				if (!href) return;
				let td = doc.create("td", { style: "text-align:center;white-space:nowrap" });
				let copy = doc.create("a", { href, title: href }, "复制");
				let offline = doc.create("a", { href, style: "margin-left:16px" }, "离线下载");
				copy.addEventListener("click", this.copyTxt);
				offline.addEventListener("click", this.offLine);
				td.appendChild(copy);
				td.appendChild(offline);
				node.appendChild(td);
			});
		};
		contentLoaded = async () => {
			ck && this.handleClick();
			// copy
			const handleCopy = tag => {
				const node = doc.querySelector(tag);
				if (!node) return;
				const href = node.innerText;
				const copy = doc.create("a", { title: href, href, style: "margin-left:16px" }, "复制");
				copy.addEventListener("click", this.copyTxt);
				node.appendChild(copy);
			};
			handleCopy("h3");
			handleCopy("span[style='color:#CC0000;']");
			// info
			const info = doc.querySelector(".col-md-3.info");
			// expBtn
			const movie = doc.querySelector(".row.movie");
			const screencap = doc.querySelector(".col-md-9.screencap");
			const exp = doc.create("input", { type: "checkbox", id: "exp" });
			const expBtn = doc.create("label", { for: "exp", id: "expBtn" });
			movie.insertBefore(exp, screencap);
			screencap.appendChild(expBtn);
			// resource
			let resBox = doc.create("p", { id: "resBox" });
			let span = doc.create("span", { class: "header" }, "已有资源:");
			resBox.appendChild(span);
			info.appendChild(resBox);
			// btnGrp
			let btnGrp = doc.create("div", {
				id: "btnGrp",
				class: "btn-group btn-group-justified",
				role: "group",
				"aria-label": "options",
			});
			const btns = { smartRes: "一键离线", refreshRes: "资源刷新" };
			Object.keys(btns).forEach(id => {
				let btn = doc.create("a", { id, class: "btn btn-default" }, btns[id]);
				btn.addEventListener("click", () => this.handleOpt(id));
				btnGrp.appendChild(btn);
			});
			info.appendChild(btnGrp);
			// table
			doc.querySelector("#magnet-table tbody tr").appendChild(
				doc.create("td", { style: "text-align:center;white-space:nowrap" }, "操作")
			);
			// photoinfo
			for (const item of doc.querySelectorAll(".photo-info")) {
				item.querySelector("span").classList.add("ellipsis");
			}
			// config
			if (!this.codeKey) {
				let [code, time] = doc.querySelectorAll("div.col-md-3.info p");
				code = code.querySelectorAll("span")[1].childNodes[0].nodeValue.trim().toUpperCase();
				time = time.childNodes[1].nodeValue.trim().toUpperCase();
				await this.config(`${code}/${time}`);
			}
			this.handleResource();
			this.handleVideo();
			this.handlePreview();
			this.handleStar();
		};
		handleClick = () => {
			const links = doc.querySelectorAll(".movie-box");
			for (const link of links) {
				const { href } = link;
				link.addEventListener("contextmenu", e => e.preventDefault());
				link.addEventListener("click", e => {
					e.preventDefault();
					GM_openInTab(href, { active: true });
				});
				link.addEventListener("mousedown", e => {
					if (e.button !== 2) return;
					e.preventDefault();
					mousePoint = e.screenX + e.screenY;
				});
				link.addEventListener("mouseup", e => {
					const num = e.screenX + e.screenY - mousePoint;
					if (e.button !== 2 || num > 5 || num < -5) return;
					e.preventDefault();
					GM_openInTab(href);
				});
			}
		};
		load = () => {
			const maxHei = 600;
			let styleHei = doc.querySelector(".col-md-9.screencap").querySelector(".bigImage img").height;
			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; }
                `);
			}
		};
		copyTxt = e => {
			e.preventDefault();
			e.stopPropagation();
			let node = e.target;
			let { title } = node;
			if (!title) return;
			GM_setClipboard(title);
			const text = node.innerText;
			node.innerText = "成功";
			setTimeout(() => {
				node.innerText = text;
			}, 1000);
		};
		// 预览大图
		handlePreview = async () => {
			let image = this.lfItem?.image;
			if (!image) {
				let res = await request(`https://javstore.net/search/${this.code}.html`);
				const link = res?.querySelector("#content_news li a")?.href;
				if (link) res = await request(link);
				image = res?.querySelector(".news a img[alt*='.th']")?.src?.replace(".th", "");
				if (!image) return;
				lf.upItem(this.codeKey, { image });
			}
			const img = doc.create("img", { src: image, title: "点击收起", style: "cursor:pointer" });
			img.addEventListener("click", () => {
				const checkbox = doc.querySelector("#exp");
				checkbox.checked = !checkbox.checked;
			});
			const append = () => doc.querySelector(".col-md-9.screencap").appendChild(img);
			if (img.complete) return append();
			img.onload = append;
		};
		// 预览视频
		handleVideo = async () => {
			let video = this.lfItem?.video;
			if (!video) {
				const res = await request(`https://www.r18.com/common/search/searchword=${this.code}/`);
				video = res?.querySelector("a.js-view-sample")?.getAttribute("data-video-high");
				if (!video) return;
				lf.upItem(this.codeKey, { video });
			}
			const title = "视频预览";
			// 打开视频弹窗
			const playVideo = e => {
				e.preventDefault();
				e.stopPropagation();
				doc.body.setAttribute("style", "overflow: hidden;");
				doc.querySelector("#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.body.setAttribute("style", "overflow: auto;");
				doc.querySelector("#mask").setAttribute("style", "display: none;");
				doc.querySelector("video").pause();
				doc.onkeydown = null;
			};
			// 视频播放窗口
			const videoNode = doc.create("video", { controls: "controls", src: video });
			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: "mask" });
			mask.appendChild(closeBtn);
			mask.appendChild(videoNode);
			doc.body.appendChild(mask);
			// 封面图点击播放
			const bImg = doc.querySelector(".bigImage img");
			bImg.setAttribute("title", title);
			bImg.addEventListener("click", playVideo);
			// ”样品图像“添加播放项目
			const thumb = doc.querySelector(".bigImage img").src;
			const box = doc.create("a", {
				class: "sample-box",
				id: "previewVideo",
				href: thumb,
				title,
			});
			box.addEventListener("click", playVideo);
			const frame = doc.create("div", { class: "photo-frame" });
			const img = doc.create("img", { src: thumb, title });
			frame.appendChild(img);
			box.appendChild(frame);
			let waterfall = doc.querySelector("#sample-waterfall");
			if (!waterfall) {
				const h4 = doc.create("h4", {}, "樣品圖像");
				waterfall = doc.create("div", { id: "sample-waterfall" });
				const ref = doc.querySelector(".clearfix");
				ref.parentNode.insertBefore(waterfall, ref);
				waterfall.parentNode.insertBefore(h4, waterfall);
			}
			const ref = waterfall.querySelector("a");
			ref ? waterfall.insertBefore(box, ref) : waterfall.appendChild(box);
		};
		// 演员列表
		handleStar = async () => {
			const nodes = doc.querySelector(".col-md-3.info").childNodes;
			let starNode = "";
			for (const node of nodes) {
				if (node.nodeType === 3 && node.nodeValue.trim() === "暫無出演者資訊") {
					starNode = node;
					break;
				}
			}
			if (!starNode) return;
			let star = this.lfItem?.star ?? [];
			if (!star.length) {
				const site = "https://javdb.com";
				let res = await request(`${site}/search?q=${this.code}`);
				const href = res.querySelector("#videos .grid-item a").getAttribute("href");
				if (href) res = await request(`${site}${href}`);
				let panels = res
					?.querySelector(".video-meta-panel")
					?.querySelectorAll(".column")[1]
					?.querySelectorAll(".panel-block");
				if (!panels) return;
				panels = panels[panels.length - 3]?.querySelector(".value")?.querySelectorAll("a") || [];
				for (const panel of panels) {
					const starName = panel.innerHTML.trim();
					if (starName) star.push(starName);
				}
				if (!star.length) return;
				lf.upItem(this.codeKey, { star });
			}
			const p = doc.create("p");
			star.map(item => {
				const span = doc.create("span", { class: "genre" });
				const a = doc.create("a", { href: `https://www.javbus.com/search/${item}` }, item);
				span.appendChild(a);
				p.appendChild(span);
			});
			starNode.parentNode.replaceChild(p, starNode);
		};
		// 已有资源(本地)
		handleResource = async () => {
			const lfItem = await lf.getItem(this.codeKey);
			let resource = lfItem?.resource;
			let upDate = lfItem?.upDate;
			const bool = !upDate || Math.floor((new Date().getTime() - upDate) / 24 / 3600 / 1000) > 3;
			if (bool) resource = await this.fetchResource();
			let resBox = doc.querySelector("#resBox");
			let olds = resBox.querySelectorAll(".genre");
			for (const old of olds) resBox.removeChild(old);
			if (!resource?.length) {
				let genre = doc.create("span", { class: "genre" }, "无");
				return resBox.appendChild(genre);
			}
			resource.map(({ link, getDate, name }) => {
				let genre = doc.create("span", { class: "genre" });
				let thunbName = name.replace(/\.\w+$/gi, "");
				thunbName = thunbName.length > 20 ? `${thunbName.substr(0, 20)}...` : thunbName;
				let a = doc.create("a", { href: link, title: `${getDate}/${name}`, target: "_blank" }, thunbName);
				genre.appendChild(a);
				resBox.appendChild(genre);
			});
		};
		// 已有资源(115)
		fetchResource = async () => {
			const code = this.code;
			let codes = [
				code,
				code.replace(/-/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"),
			];
			let { data } = await request("https://webapi.115.com/files/search", "GET", {
				search_value: encodeURIComponent(codes.join(" ")),
				format: "json",
			});
			let resource = [];
			if (data?.length) {
				const reg = new RegExp(`(${codes.join("|")})`, "gi");
				data = data.filter(({ n, play_long }) => n.match(reg) && play_long > 0);
				resource = data.map(item => {
					return {
						fid: item.fid,
						cid: item.cid,
						dir: `https://115.com/?cid=${item.cid}&offset=0&mode=wangpan`,
						link: `https://v.anxia.com/?pickcode=${item.pc}`,
						getDate: item.t,
						name: item.n,
					};
				});
			}
			await lf.upItem(this.codeKey, { upDate: new Date().getTime(), resource });
			return resource;
		};
		// 离线下载
		offLine = async e => {
			e.preventDefault();
			e.stopPropagation();
			let node = e.target;
			let { href } = node;
			if (!href) return;
			GM_setClipboard(href);
			const text = node.innerText;
			node.innerText = "请求中...";
			let zh = !!node.parentNode.parentNode
				.querySelector("td")
				.querySelector("a.btn.btn-mini-new.btn-warning.disabled");
			const obj = await this.offLineDownload({ link: href, zh });
			node.innerText = text;
			notifiy(obj);
		};
		// 排队离线/资源刷新
		handleOpt = async action => {
			const node = doc.querySelector(`#${action}`);
			node.classList.toggle("disabled");
			const text = node.innerText;
			node.innerText = "请求中...";

			if (action === "refreshRes") {
				await lf.upItem(this.codeKey, { upDate: 0 });
				await this.handleResource();
			}
			if (action === "smartRes") {
				const trs = doc.querySelector("#magnet-table").querySelectorAll("tr");
				let magnetArr = [];
				for (let index = 1; index < trs.length; index++) {
					let item = { zh: false, size: 0, date: 0 };
					const elem = trs[index];
					let [zh, size, date] = elem.querySelectorAll("td");
					for (const a of zh.querySelectorAll("a")) {
						item.zh = a.innerText.trim() === "字幕";
						if (item.zh) break;
					}
					size = size.querySelector("a").innerText.trim().replace(/gb/gi, "");
					if (/mb/gi.test(size)) size = (parseInt(size, 10) / 1024).toFixed(2);
					item.size = Number(size);
					date = date.querySelector("a");
					item.date = date.innerText.trim().replace(/-/g, "");
					item.link = date.getAttribute("href");
					magnetArr.push(item);
				}
				magnetArr.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 < magnetArr.length; index++) {
					const obj = await this.offLineDownload(magnetArr[index]);
					if (obj?.icon !== "info") {
						notifiy(obj);
						break;
					}
					if (index !== magnetArr.length - 1) continue;
					notifiy(
						"一键离线失败",
						"远程未查找到新增资源,接口失效或资源被审核",
						"fail",
						"http://115.com/?tab=offline&mode=wangpan"
					);
				}
			}

			node.innerText = text;
			node.classList.toggle("disabled");
		};
		// 请求离线&结果查询
		offLineDownload = async ({ link, zh }) => {
			if (!rootId) {
				const rid = prompt("输入115离线根目录cid", "");
				if (!rid) return;
				GM_setValue("rid", rid);
				rootId = rid;
			}

			let fname = doc.querySelector("h3").childNodes[0].nodeValue;
			if (zh) fname = `【中文字幕】${fname}`;
			let notifiyObj = {
				title: "操作失败,115未登录",
				text: "请登录115账户后再离线下载",
				icon: "fail",
				clickUrl: "http://115.com/?mode=login",
			};
			let res = await request("http://115.com/", "GET", {
				ct: "offline",
				ac: "space",
				_: new Date().getTime(),
			});
			if (!res?.sign) return notifiyObj;
			const { sign, time } = res;
			res = await request(
				"http://115.com/web/lixian/?ct=lixian&ac=add_task_url",
				"POST",
				`url=${encodeURIComponent(link.substr(0, 60))}&uid=0&sign=${sign}&time=${time}`
			);
			let { state, errcode, error_msg } = res;
			notifiyObj = {
				title: "离线失败",
				text: error_msg,
				icon: "info",
				clickUrl: "http://115.com/?tab=offline&mode=wangpan",
			};
			if (`${errcode}` === "911") {
				notifiyObj.title += ",账号异常";
				notifiyObj.text = "验证后正常使用";
				notifiyObj.icon = "fail";
			}
			if (!state) return notifiyObj;

			// 获取旧的本地缓存数据
			let lfItem = await lf.getItem(this.codeKey);
			// 远程获取搜索结果
			await delay(2000);
			let resource = await this.fetchResource();
			this.handleResource();
			// 当前日期
			let date = 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();
			date = `${Y}-${M}-${D}`;

			const newRes = resource.filter(item => item.getDate === date);
			// 搜索结果资源数至少比本地缓存多且获取日期为当前日期
			if (resource.length <= lfItem.resource.length || newRes.length < 1) {
				notifiyObj.text = "未找到新增资源,接口失效或资源审核";
				return notifiyObj;
			}
			this.afterAction(
				newRes.filter(item => item.name.indexOf(fname) === -1),
				fname
			);
			return {
				title: "离线成功",
				text: "点击跳转",
				icon: "success",
				clickUrl: `https://115.com/?cid=${rootId}&offset=0&mode=wangpan`,
			};
		};
		// 离线后重命名,移动,移除原目录等
		afterAction = async (items, fname) => {
			// 重命名
			for (const { fid } of items) {
				request(
					"http://webapi.115.com/files/edit",
					"POST",
					`fid=${fid}&file_name=${encodeURIComponent(fname)}`
				);
			}
			const params = arr => arr.reduce((acc, cur, idx, src) => `${acc}&fid[${idx}]=${cur}`, "");
			const fids = Array.from(new Set(items.map(item => item.fid)));
			const cids = Array.from(new Set(items.filter(item => item !== rootId).map(item => item.cid)));
			// 移动
			const move_proid = `${new Date()}_${~(100 * Math.random())}_0`;
			await request(
				"https://webapi.115.com/files/move",
				"POST",
				`pid=${rootId}&move_proid=${move_proid}${params(fids)}`
			);
			// 删除
			request("https://webapi.115.com/rb/delete", "POST", `pid=${rootId}&ignore_warn=1${params(cids)}`);
			lf.upItem(this.codeKey, { upDate: 0 });
			await delay(1000);
			this.handleResource();
		};
	}

	class JavBusScript {
		waterfall = new Waterfall();
		genre = {
			docStart: () => {
				GM_addStyle(`
                footer { display: none${SUFFIX}; }
                button.btn.btn-danger.btn-block.btn-genre {
                    position: fixed${SUFFIX};
                    bottom: 0${SUFFIX};
                    margin: 0${SUFFIX};
                    left: 0${SUFFIX};
                    border: 0${SUFFIX};
                    border-radius: 0${SUFFIX};
                }
                `);
			},
			contentLoaded: () => {
				if (!doc.querySelector("button.btn.btn-danger.btn-block.btn-genre")) return;
				let box = doc.querySelectorAll(".genre-box");
				box[box.length - 1].setAttribute("style", "margin-bottom: 65px;");
			},
		};
		forum = {
			docStart: function () {
				GM_addStyle(`
                .bcpic, .banner728 {
                    display: none${SUFFIX};
                }
                .jav-button {
                    margin-top: -3px${SUFFIX};
                }
                `);
			},
			// contentLoaded: function () {},
			// load: function () {},
		};
		details = new Details();
	}

	const darkMode = path => {
		if (path === "forum") return;
		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 Pink = "rgb(255,55,95)";
		const Red = "rgb(255,69,58)";
		const Yellow = "rgb(255,214,10)";
		GM_addStyle(`
        *:not(span) {
            border-color: ${Grey}${SUFFIX};
            text-shadow: none${SUFFIX};
        }
        body, footer {
            background: ${Background}${SUFFIX};
            color: ${SecondaryLabelColor}${SUFFIX};
        }
        img {
		    filter: brightness(90%)${SUFFIX};
		}
        nav {
            background: ${SecondaryBackground}${SUFFIX};
        }
        input {
            background: ${Background}${SUFFIX};
        }
        *::placeholder {
            color: ${SecondaryLabelColor}${SUFFIX};
        }
        input, a, h1, h2, h3, h4, h5, h6 {
            color: ${LabelColor}${SUFFIX};
            box-shadow: none${SUFFIX};
            outline: none${SUFFIX};
        }
        .btn.disabled, .btn[disabled], fieldset[disabled] .btn {
            opacity: .85${SUFFIX};
        }
        button, .btn-default, .input-group-addon {
            background: ${Grey}${SUFFIX};
            color: ${LabelColor}${SUFFIX};
        }
        .btn-primary {
            background: ${Blue}${SUFFIX};
            border-color: ${Blue}${SUFFIX};
        }
        .btn-success {
            background: ${Green}${SUFFIX};
            border-color: ${Green}${SUFFIX};
        }
        .btn-danger {
            background: ${Red}${SUFFIX};
            border-color: ${Red}${SUFFIX};
        }
        .btn-warning {
            background: ${Orange}${SUFFIX};
            border-color: ${Orange}${SUFFIX};
        }
        .navbar-nav>.active>a, .navbar-nav>.active>a:focus, .navbar-nav>.active>a:hover, .navbar-nav>.open>a, .dropdown-menu {
            background: ${Background}${SUFFIX};
        }
        .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
            background: ${Grey}${SUFFIX};
        }
        .pagination .active a {
            border: none${SUFFIX};
            color: ${LabelColor}${SUFFIX};
        }
        .pagination>li>a, .pagination>li>span {
            background-color: ${Grey}${SUFFIX};
            border: none${SUFFIX};
            color: ${LabelColor}${SUFFIX};
        }
        tr, .modal-content, .alert {
            background: ${SecondaryBackground}${SUFFIX};
            box-shadow: none${SUFFIX};
        }
        tr:hover {
            background: ${Grey}${SUFFIX};
        }
        `);
		if (path === "waterfall") {
			GM_addStyle(`
            .item a {
                background: ${SecondaryBackground}${SUFFIX};
            }
            .photo-info {
                background: ${SecondaryBackground}${SUFFIX};
                color: ${LabelColor}${SUFFIX};
            }
            date {
                color: ${SecondaryLabelColor}${SUFFIX};
            }
            .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}${SUFFIX};
            }
            `);
		}
		if (path === "genre") {
			GM_addStyle(`
            .genre-box {
                background-color: ${SecondaryBackground}${SUFFIX};
            }
            `);
		}
		if (path === "details") {
			GM_addStyle(`
            .movie, .sample-box, .movie-box, .photo-info {
                background: ${SecondaryBackground}${SUFFIX};
            }
            .photo-info {
                color: ${LabelColor}${SUFFIX};
            }
            .avatar-box, .avatar-box span, .info ul li, .info .star-name {
                background: ${SecondaryBackground}${SUFFIX};
                border-color: ${Grey}${SUFFIX};
                color: ${LabelColor}${SUFFIX};
            }
            `);
		}
	};

	if (/javbus\.com/g.test(host)) {
		const menus = [
			{ title: "点击事件", name: "ck", val: ck, key: "c" },
			{ title: "黑暗模式", name: "dm", val: dm, key: "d" },
			{ title: "滚动加载", name: "lm", val: lm, key: "s" },
		];
		for (const { title, name, val, key } of menus) {
			GM_registerMenuCommand(
				`${val ? "关闭" : "开启"}${title}`,
				() => {
					GM_setValue(name, !val);
					location.reload();
				},
				key
			);
		}

		lf.config({ name: "JBDB", storeName: "AVS" });

		GM_addStyle(`
        .ad-box { 
            display: none${SUFFIX}; 
        }
        .ellipsis {
            overflow : hidden;
            text-overflow: ellipsis;
            display: -webkit-box;
            -webkit-line-clamp: 1;
            -webkit-box-orient: vertical;
        }
        .line-4 {
            -webkit-line-clamp: 4;
        }
        `);

		const pathReg = {
			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,
		};
		const path = Object.keys(pathReg).filter(key => pathReg[key].test(pathname))[0];
		if (!path) return;
		dm && darkMode(path);
		let jav = new JavBusScript();
		jav = jav[path];
		if (!jav) return;
		const { docStart, contentLoaded, load } = jav;

		docStart && jav.docStart();
		contentLoaded && doc.addEventListener("DOMContentLoaded", jav.contentLoaded);
		load && window.addEventListener("load", jav.load);
	}

	if (/captchaapi\.115\.com/g.test(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);
			});
		});
	}
})();