JavBus工具

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

2021/11/29のページです。最新版はこちら。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name            JavBus工具
// @description     暗黑模式、滚动加载、预览视频、离线下载(115)...
// @version         0.2.0
// @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/localforage@1.10.0/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         *
// @license         MIT
// @namespace https://greasyfork.org/users/175514
// ==/UserScript==

/**
 * 非完全测试,自用Edge
 * TODO:
 * ✔️ 暗黑模式
 * ✔️ 点击事件(新窗口打开,左键前台,右键后台)
 * ✔️ 滚动加载
 * ✔️ 获取预览大图(javstore.net)
 * ✔️ 获取预览视频(r18.com),如有资源点击封面图或样品图像处进行预览
 * ✔️ 获取演员名单(javdb.com)
 * ✔️ 离线下载(请求离线成功2秒后查询结果汇报),自动修改文件名称,(如已设置操作目录cid,自动移动视频文件至对应目录并删除原目录)
 * ✔️ 一键离线(字幕>尺寸>日期)排队执行离线下载,成功一结束排队
 * ✔️ 115账号验证弹窗
 * ✔️ 查询115是否已有离线资源
 * ✔️ 查询115是否已有离线资源并缓存(默认3天有效),3天后重新查询,支持手动刷新“资源刷新”
 * ✔️ 根据缓存数据匹配列表页已有资源状态,添加已有资源列表页
 */
 
(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;
	const 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 insertBefore = (el, htmlString) => el.insertAdjacentHTML("beforebegin", htmlString);
	const insertAfter = (el, htmlString) => el.insertAdjacentHTML("afterend", htmlString);
	const appendBefore = (el, htmlString) => el.insertAdjacentHTML("afterbegin", htmlString);
	const appendAfter = (el, htmlString) => el.insertAdjacentHTML("beforeend", htmlString);

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

	const scriptStart = () => {
		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();
			}
		});

		const nav = `<a href="https://www.javbus.com/resource">资源</a>`;
		let navbar = doc.querySelector("#navbar .nav.navbar-nav");
		if (navbar) appendAfter(navbar, `<li class="resource">${nav}</li>`);
		navbar = doc.querySelector("#toptb .wp .z ul");
		if (navbar) appendAfter(navbar, `<li class="nav-title nav-inactive">${nav}</li>`);
	};

	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 {
                position: relative;
                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};
            }
			`);
			GM_addStyle(`
            .hasRes {
                background-color: rgba(0,0,0,.2);
                position: absolute;
                height: 100%;
                width: 100%;
                top: 0;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .playBtn {
                opacity: .85;
            }
            `);
		};
		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);
			// add element
			const waterfall = doc.querySelector("#waterfall");
			appendBefore(waterfall, `<div class="item-sizer"></div><div class="gutter-sizer"></div>`);
			insertAfter(
				waterfall,
				`<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.querySelectorAll("div.container-fluid")[1].querySelector(".row").classList.add("scrollBox");
		};
		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) => {
			const items = node.querySelectorAll(".item");
			for (const item of items) {
				let [code, date] = item.querySelectorAll("date");
				if (code && date) {
					code = code.textContent;
					date = date.textContent;
					const codeKey = `${code.trim().toUpperCase()}/${date.trim().toUpperCase()}`;
					lf.keys().then(keys => {
						if (!keys.includes(codeKey)) return;
						lf.getItem(codeKey).then(val => {
							const resource = val?.resource;
							if (!resource?.length) return;
							const hasRes = doc.create("div", { class: "hasRes" });
							const playBtn = doc.create(
								"button",
								{ class: "btn btn-primary playBtn", title: "点击播放" },
								"已有资源"
							);
							playBtn.addEventListener("click", e => {
								e.stopPropagation();
								e.preventDefault();
								window.open(resource[0].link);
							});
							hasRes.appendChild(playBtn);
							item.querySelector(".photo-frame").appendChild(hasRes);
						});
					});
				}

				const info = item.querySelector("a .photo-info span:not(.mleft)");
				if (!info) return;
				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;']");

			// expBtn
			const screencap = doc.querySelector(".col-md-9.screencap");
			insertBefore(screencap, `<input type="checkbox" id="exp">`);
			appendAfter(screencap, `<label for="exp" id="expBtn"></label>`);

			const info = doc.querySelector(".col-md-3.info");
			// resource
			appendAfter(
				info,
				`<p id="resBox"><span class="header">已有资源:</span><span class="genre">查询中...</span></p>`
			);

			// btnGrp
			const 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
			appendAfter(
				doc.querySelector("#magnet-table tbody tr"),
				`<td style="text-align:center;white-space:nowrap">操作</td>`
			);

			// photoinfo
			for (const item of doc.querySelectorAll(".photo-info span")) {
				item.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 .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", () => {
				doc.querySelector("#exp").checked = false;
			});
			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.currentTime = 5;
			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 = bImg.src;
			const box = doc.create("a", {
				class: "sample-box",
				id: "previewVideo",
				href: thumb,
				title,
			});
			box.addEventListener("click", playVideo);
			appendAfter(box, `<div class="photo-frame"><img src="${thumb}" title="${title}"></div>`);

			if (!doc.querySelector("#sample-waterfall")) {
				insertBefore(doc.querySelector("div.clearfix"), `<div id="sample-waterfall"></div>`);
				insertBefore(doc.querySelector("div#sample-waterfall"), `<h4>樣品圖像</h4>`);
			}
			const waterfall = doc.querySelector("div#sample-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");
			const urlStr = "https://www.javbus.com/search/";
			appendAfter(
				p,
				star.reduce((acc, cur) => `${acc}<span class="genre"><a href="${urlStr}${cur}">${cur}</a></span>`, "")
			);
			starNode.parentNode.replaceChild(p, starNode);
		};
		// 已有资源(本地)
		handleResource = async () => {
			const lfItem = await lf.getItem(this.codeKey);

			let resource = lfItem?.resource ?? [];
			const upDate = lfItem?.upDate;
			const bool = !upDate || Math.floor((new Date().getTime() - upDate) / 24 / 3600 / 1000) > 3;
			if (bool) resource = await this.fetchResource();

			const resBox = doc.querySelector("#resBox");
			for (const old of resBox.querySelectorAll(".genre")) resBox.removeChild(old);
			if (!resource?.length) return appendAfter(resBox, `<span class="genre">无</span>`);
			const child = resource.reduce((acc, { link, getDate, name }) => {
				let thunbName = name.replace(/\.\w+$/gi, "");
				thunbName = thunbName.length > 20 ? `${thunbName.substr(0, 20)}...` : thunbName;
				return `${acc}<span class="genre"><a href="${link}" title="${getDate}/${name}" target="_blank">${thunbName}</a></span>`;
			}, "");
			appendAfter(resBox, child);

			if (!lfItem?.thumb) lf.upItem(this.codeKey, { thumb: doc.querySelector(".screencap .bigImage img").src });
			if (!lfItem?.title) lf.upItem(this.codeKey, { title: doc.querySelector("h3").childNodes[0].textContent });
			if (!lfItem?.href) lf.upItem(this.codeKey, { href: location.href });
		};
		// 已有资源(115)
		fetchResource = async (lname = "") => {
			let 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"),
			];
			if (lname) codes.unshift(lname);
			let { data } = await request("https://webapi.115.com/files/search", "GET", {
				search_value: encodeURIComponent(codes.join(" ")),
				format: "json",
			});
			let resource = [];
			if (data && 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,
						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();
			const node = e.target;
			const { href } = node;
			if (!href) return;
			GM_setClipboard(href);
			const text = node.innerText;
			node.innerText = "请求中...";
			const zh = !!node.parentNode.parentNode.querySelector("td a.btn.btn-mini-new.btn-warning.disabled");
			const lname = node.parentNode.parentNode.querySelector("td a").innerText.trim();
			const obj = await this.offLineDownload({ link: href, zh, lname });
			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, lname: "" };
					const elem = trs[index];
					let [zh, size, date] = elem.querySelectorAll("td");
					if (!zh || !size || !date) continue;
					const ls = zh.querySelectorAll("a");
					item.lname = ls[0].innerText.trim();
					for (const a of ls) {
						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, lname }) => {
			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}`
			);
			const { 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)) ?? {};
			if (!lfItem?.resource) lfItem.resource = [];
			// 远程获取搜索结果
			await delay(2000);
			let resource = await this.fetchResource(lname);
			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;
			}
			for (let index = 0; index < newRes.length; index++) {
				const { name } = newRes[index];
				const suffix = name.split(".").pop().toLowerCase();
				newRes[index]["rename"] = `${fname}.${suffix}`;
			}
			this.afterAction(newRes.filter(item => item.name.indexOf(fname) === -1));
			return {
				title: "离线成功",
				text: "点击跳转",
				icon: "success",
				clickUrl: `https://115.com/?cid=${rootId ?? 0}&offset=0&mode=wangpan`,
			};
		};
		/**
		 * TODO:
		 * 115链接任务列表(可用于查询失败链接任务)
		 */
		handleTaskList = async () => {
			const { sign, time } = await request("http://115.com/", "GET", {
				ct: "offline",
				ac: "space",
				_: new Date().getTime(),
			});
			let { tasks } = await request(
				"https://115.com/web/lixian/?ct=lixian&ac=task_lists",
				"POST",
				`page=1&uid=0&sign=${sign ?? ""}&time=${time ?? new Date().getTime()}`
			);
			// console.log(tasks);
			// del_path
			// status 2成功 1下载中 -1下载失败
			// url
		};
		// 离线后重命名,移动,移除原目录等
		afterAction = async items => {
			// rename
			for (const { fid, rename } of items) {
				request(
					"http://webapi.115.com/files/edit",
					"POST",
					`fid=${fid}&file_name=${encodeURIComponent(rename)}`
				);
			}
			if (rootId) {
				const params = arr => arr.reduce((acc, cur, idx) => `${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)));
				// move
				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)}`
				);
				// del
				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: () => {
				GM_addStyle(`
		        .bcpic, 
                .banner728,
                .sd.sd_allbox > div:last-child {
		            display: none${SUFFIX};
		        }
		        .jav-button {
		            margin-top: -3px${SUFFIX};
		        }
                #toptb {
                    position: fixed${SUFFIX};
                    top: 0${SUFFIX};
                    left: 0${SUFFIX};
                    right: 0${SUFFIX};
                    z-index: 999${SUFFIX};
                }
                #wp {
                    margin-top: 55px${SUFFIX};
                }
		        `);
			},
			contentLoaded: () => {},
		};
		resource = {
			docStart: () => {
				// layout
				GM_addStyle(`
                .alert.alert-danger.alert-page.error-page, footer {
                    display: none !important;
                }
                .resItem {
                    margin: 15px 0;
                    cursor: pointer;
                    border-radius: 4px;
                    overflow: hidden;
                    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
                }
                .thumb {
                    padding-bottom: 67%;
                    background-repeat: no-repeat;
	                background-size: cover;
                    background-color: #fff;
                    position: relative;
                }
                .info {
                    padding: 15px;
                }
                .title {
                    margin-bottom: 6px;
                }
                `);
				// light
				GM_addStyle(`
                .info {
                    background-color: #fff;
                    color: #333;
                }
                `);
				// dark
				if (!dm) return;
				GM_addStyle(`
                .info {
                    background-color: ${SecondaryBackground};
                }
                .title {
                    color: ${LabelColor};
                }
                `);
			},
			contentLoaded: async () => {
				doc.title = "已有资源(0)";
				const navbar = doc.querySelector("#navbar .nav.navbar-nav");
				navbar.querySelector(".active").classList.remove("active");
				navbar.querySelector(".resource").classList.add("active");
				const keys = await lf.keys();
				let nodes = [];
				for (const key of keys) {
					const item = await lf.getItem(key);
					const resource = item?.resource;
					if (!resource?.length) continue;
					nodes.push({ ...item, key: key.split("/")[0] });
				}
				if (!nodes?.length) return;
				doc.title = `已有资源(${nodes.length})`;
				nodes = nodes.reduce((pre, { thumb, title, resource, href, key, upDate }) => {
					let date = new Date(upDate);
					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}`;
					return `${pre}
                        <div class="col-lg-3 col-md-4 col-sm-6">
                            <div class="resItem">
                                <div 
                                    class="thumb" 
                                    title="点击播放" 
                                    data-link="${resource[0].link}" 
                                    style="background-image: url(${thumb})"
                                >
                                </div>
                                <a href="${href ?? "/" + key}">
                                    <div class="info">
                                        <div class="ellipsis title" title="${title}">${title}</div>
                                        <button class="btn btn-xs btn-primary" disabled="disabled">
                                            更新:${date}
                                        </button>
                                        <button class="btn btn-xs btn-danger" disabled="disabled">
                                            资源:${resource.length}
                                        </button>
                                    </div>
                                </a>
                            </div>
                        </div>`;
				}, "");
				appendAfter(doc.querySelector(".container-fluid .row"), nodes);
				for (const item of doc.querySelectorAll(".resItem")) {
					const thumb = item.querySelector(".thumb");
					thumb.addEventListener("click", () => window.open(thumb.getAttribute("data-link")));
					if (!ck) continue;
					const link = item.querySelector("a");
					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);
					});
				}
			},
		};
		details = new Details();
	}

	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)";
	const darkMode = path => {
		if (path === "forum") return;
		GM_addStyle(`
        ::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }
        ::-webkit-scrollbar-thumb {
            border-radius: 4px;
            background-color: ${Grey};
        }
        *:not(span) {
            border-color: ${Grey}${SUFFIX};
            outline: ${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};
        }
        button, 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 === "forum") {
			GM_addStyle(`
            #toptb,
            .mn > div,
            .biaoqicn_show.slidebar a:not(.on),
            .new4_list_top .dspanonhover,
            div[id*="con_NewOne_"],
            .frame,
            .frame-tab,
            .main-right-p15,
            .bm.bmw.cl,
            .bm.bmw.cl .bm_h.cl {
                background: ${SecondaryBackground}${SUFFIX};
            }
            .jav-footer,
            .nav-title.nav-active a,
            .menu-body-panel.login-detail-wrap,
            .menu-body-panel .icon-arrow-t,
            .new4_list_top,
            .comment-excerpt {
                background: ${Background}${SUFFIX};
            }
            .main-right-zuixin .comment-excerpt:before {
                border-bottom-color: ${Background}${SUFFIX};
            }
            .main-right-tit span {
                color: ${LabelColor}${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
			);
		}
		GM_registerMenuCommand(
			"离线后操作根目录cid",
			() => {
				const rid = prompt("用于离线后移动,删除等操作", rootId);
				GM_setValue("rid", rid);
				location.reload();
			},
			"a"
		);

		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,
			resource: /^\/resource$/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;
		jav.docStart();
		doc.addEventListener("DOMContentLoaded", () => {
			scriptStart();
			jav.contentLoaded();
		});
		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);
			});
		});
	}
})();