您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一站式体验,JavBus & JavDB 兼容
当前为
// ==UserScript== // @name JavScript // @namespace JavScript@blc // @version 4.5.0 // @author blc // @description 一站式体验,JavBus & JavDB 兼容 // @include * // @icon https://s1.ax1x.com/2022/04/01/q5lzYn.png // @resource success https://s1.ax1x.com/2022/04/01/q5l2LD.png // @resource info https://s1.ax1x.com/2022/04/01/q5lyz6.png // @resource warn https://s1.ax1x.com/2022/04/01/q5lgsO.png // @resource error https://s1.ax1x.com/2022/04/01/q5lcQK.png // @supportURL https://t.me/+bAWrOoIqs3xmMjll // @connect * // @run-at document-start // @grant unsafeWindow // @grant GM_removeValueChangeListener // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @grant GM_getResourceURL // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_setClipboard // @grant GM_deleteValue // @grant GM_addElement // @grant GM_listValues // @grant GM_openInTab // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_info // @license GPL-3.0-only // @compatible chrome last 2 versions // @compatible edge last 2 versions // ==/UserScript== // prettier-ignore (function () { const NUM_REGEX = /\d+\.?\d*/g; const ZH_REGEX = /中文|字幕|中字|-c(?!d)/i; const CODE_REGEX = /^[a-z0-9]+(-|\w)+/i; const VR_REGEX = /(?<![a-z])VR(?![a-z-])/i; const FC2_REGEX = /^FC2-/i; const REP = "%s"; const DOC = document; const VOID = "javascript:void(0)"; const PICK_RUL = "https://v.anxia.com/?pickcode="; const FILE_RUL = `https://115.com/?cid=${REP}&offset=0&mode=wangpan`; const TAB_NAME = { img: "大图", video: "预览", player: "视频" }; const addListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (type, ...args) { if (type !== "XContentLoaded") return addListener.call(this, type, ...args); document.readyState !== "loading" ? args[0]() : addListener.call(this, "DOMContentLoaded", ...args); }; // capture const captureJump = () => { const { hash } = location; if (!hash?.includes("#jump#")) return; const code = hash.split("#").at(-1); if (!CODE_REGEX.test(code)) return; DOC.addEventListener( "XContentLoaded", () => { const capture = captureQuery(code)?.href; if (capture) location.replace(capture); }, { once: true } ); }; const captureQuery = (code, { body } = DOC) => { const nodeList = body.querySelectorAll(":any-link"); const { length } = nodeList; if (!length) return; const { regex } = codeParse(code); code = code.toUpperCase(); const res = []; for (let i = 0; i < length; i++) { const { href = "", textContent = "", innerHTML = "" } = nodeList[i]; if (!href || /^#|javascript/i.test(href)) continue; const str = [textContent, innerHTML] .map(item => item?.trim() ?? "") .filter(Boolean) .join("&") .toUpperCase(); if (!str || !regex.test(str)) continue; if (!str.includes(code)) { res.push({ href, zh: false }); continue; } if (ZH_REGEX.test(str)) return { href, zh: true }; res.unshift({ href, zh: false }); const links = body.querySelectorAll(`a[href="${href}"]`); if (Array.from(links).some(item => ZH_REGEX.test(item.textContent))) return { href, zh: true }; } if (res.length) return res[0]; }; const codeParse = code => { const _ = FC2_REGEX.test(code) ? "|_" : ""; code = code.split("-").map((item, index) => (index ? item.replace(/^0/, "") : item)); return { prefix: code[0], regex: new RegExp(`(?<![a-z])${code.join(`(0|-${_}){0,4}`)}(?!\\d)`, "i"), }; }; captureJump(); // match const MatchDomains = [ { domain: "JavDB", regex: /javdb\d*\.com/ }, { domain: "JavBus", regex: /(jav|bus|dmm|see|cdn|fan){2}\.[a-z]{2,}/ }, { domain: "Drive115", regex: /captchaapi\.115\.com/ }, ]; const Domain = MatchDomains.find(item => item.regex.test(location.host))?.domain; if (!Domain) return; // request const request = (url, data, method = "GET", options = {}) => { method = method ? method.toUpperCase().trim() : "GET"; if (!url || !["GET", "HEAD", "POST"].includes(method)) return; if (Object.prototype.toString.call(data) === "[object Object]") { data = Object.keys(data) .map(key => `${key}=${encodeURIComponent(data[key])}`) .join("&"); } const { responseType, headers = {} } = options; if (method === "GET") { options.responseType = responseType ?? "document"; if (data) { let joiner = "?"; if (url.includes(joiner)) joiner = /\?|&$/.test(url) ? "" : "&"; url = `${url}${joiner}${data}`; } } if (method === "POST") { options.responseType = responseType ?? "json"; options.headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", ...headers }; } return new Promise(resolve => { GM_xmlhttpRequest({ url, data, method, timeout: 30000, onload: ({ status, response }) => { if (status >= 400) response = false; if (response && ["", "text"].includes(options.responseType)) { if (/<\/?[a-z][\s\S]*>/i.test(response)) { response = new DOMParser().parseFromString(response, "text/html"); } else if (/^\{.*\}$/.test(response)) { response = JSON.parse(response); } } resolve(response ?? true); }, ontimeout: () => resolve(false), onerror: () => resolve(false), ...options, }); }); }; // utils const notify = ({ text = "点击跳转115", type = "error", clickUrl = "https://115.com/", setTab, ...details }) => { GM_notification({ text: text || GM_info.script.name, image: GM_getResourceURL(type), highlight: false, silent: true, timeout: 5000, onclick: () => openInTab(clickUrl), ...details, }); if (setTab) setTabBar({ type, title: details.title }); }; const addPrefetch = arr => { arr = arr.filter(Boolean); if (!arr.length) return; DOC.head.insertAdjacentHTML("beforeend", arr.map(item => `<link rel="prefetch" href="${item}">`).join("")); }; const setTabBar = ({ type = GM_info.script.icon, title }) => { if (title) DOC.title = title; type = type.includes("http") ? type : GM_getResourceURL(type); let icons = DOC.querySelectorAll("link[rel*='icon']"); if (!icons?.length) icons = [DOC.create("link", { type: "image/x-icon", rel: "shortcut icon" })]; icons.forEach(node => { node.href = type; DOC.getElementsByTagName("head")[0].append(node); }); }; const openInTab = (url, active = true, options = {}) => { if (url) return GM_openInTab(url, { active: !!active, setParent: true, ...options }); }; const imgLoaded = img => img.classList.add("x-in"); const fadeInImg = node => { const img = node.querySelector("img"); if (!img) return; img.title = ""; if (img.complete && img.naturalHeight !== 0) return imgLoaded(img); img.onload = () => imgLoaded(img); }; const addCopy = (selectors, attrs = {}) => { const node = typeof selectors === "string" ? DOC.querySelector(selectors) : selectors; const _attrs = { "data-copy": node.textContent, class: "x-ml", href: VOID }; const target = DOC.create("a", { ..._attrs, ...attrs }, "复制"); target.addEventListener("click", e => handleCopy(e, "成功")); node.append(target); }; const handleCopy = (e, tip = "复制成功", copy) => { const { target } = e; copy = copy ? copy : target?.dataset?.copy?.trim() ?? ""; if (!copy) return; e.preventDefault(); e.stopPropagation(); GM_setClipboard(copy); const { textContent = "" } = target; target.textContent = tip || "复制成功"; const timer = setTimeout(() => { clearTimeout(timer); target.textContent = textContent; }, 300); }; const getDate = (timestamp, joiner = "-") => { const date = timestamp ? new Date(timestamp) : new Date(); const Y = date.getFullYear(); const M = `${date.getMonth() + 1}`.padStart(2, "0"); const D = `${date.getDate()}`.padStart(2, "0"); return `${Y}${joiner}${M}${joiner}${D}`; }; const transToBytes = (sizeStr = "") => { const sizeNum = sizeStr?.match(NUM_REGEX)?.[0] ?? 0; if (sizeNum <= 0) return 0; const matchList = [ { unit: /byte/gi, transform: size => size }, { unit: /kb/gi, transform: size => size * 1000 }, { unit: /mb/gi, transform: size => size * 1000 ** 2 }, { unit: /gb/gi, transform: size => size * 1000 ** 3 }, { unit: /kib/gi, transform: size => size * 1024 }, { unit: /mib/gi, transform: size => size * 1024 ** 2 }, { unit: /gib/gi, transform: size => size * 1024 ** 3 }, ]; return ( matchList .find(({ unit }) => unit.test(sizeStr)) ?.transform(sizeNum) ?.toFixed(2) ?? 0 ); }; DOC.create = (tag, attrs = {}, child) => { const node = DOC.createElement(tag); for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value); if (child) node.append(child); return node; }; const createVideo = (sources, attrs = {}) => { if (typeof sources === "string") sources = [{ src: sources, type: "video/mp4" }]; const video = DOC.create("video", attrs); video.controls = true; video.currentTime = 3; video.volume = localStorage.getItem("volume") ?? 0; video.preload = "none"; video.append(...sources.map(item => DOC.create("source", item))); let timer = null; video.addEventListener("volumechange", ({ target }) => { if (timer) clearTimeout(timer); timer = setTimeout(() => localStorage.setItem("volume", target.volume), 1000); }); video.addEventListener("keyup", ({ code, target }) => { if (code === "Enter") { target.requestFullscreen(); target.play(); } if (code === "KeyM") target.muted = !target.muted; }); return video; }; const paramParse = (param, filter, separator = "#") => { return unique(param.split(separator).filter(item => filter.includes(item))); }; const unique = (arr, key) => { if (!key) return Array.from(new Set(arr)); return unique(arr.map(e => e[key].toLowerCase())).map(e => arr.find(x => x[key].toLowerCase() === e)); }; 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 delay = (s = 1) => { return new Promise(r => { setTimeout(r, s * 1000); }); }; class Store { static init() { const date = getDate(); const cdKey = "CD"; if (GM_getValue(cdKey, "") !== date) { GM_setValue(cdKey, date); GM_setValue("DETAILS", {}); GM_setValue("RESOURCE", {}); GM_setValue("VERIFY_STATUS", ""); } const expired = getDate(new Date() - 1000 * 60 * 60 * 24 * 30); if ((localStorage.getItem(cdKey) ?? expired) > expired) return; localStorage.clear(); localStorage.setItem(cdKey, date); } static getDetail(key) { const localDetail = { img: localStorage.getItem(`${key}_img`) ?? undefined, video: localStorage.getItem(`${key}_video`) ?? undefined, }; const details = GM_getValue("DETAILS", {}); return { ...localDetail, ...(details[key] ?? {}) }; } static upDetail(key, val) { const details = GM_getValue("DETAILS", {}); details[key] = { ...(details[key] ?? {}), ...val }; GM_setValue("DETAILS", details); if (FC2_REGEX.test(key)) return; val = JSON.stringify(val, ["img", "video"]); if (val === "{}") return; for (const [_key, _val] of Object.entries(JSON.parse(val))) { localStorage.setItem(`${key}_${_key}`, _val); } } static getResource(key) { const resource = GM_getValue("RESOURCE", {}); return resource[key]; } static upResource(key, val) { const resource = GM_getValue("RESOURCE", {}); resource[key] = val; GM_setValue("RESOURCE", resource); } static getVerifyStatus() { return GM_getValue("VERIFY_STATUS", ""); } static setVerifyStatus(val) { GM_setValue("VERIFY_STATUS", val); } } class Apis { static async fetchBlogJav(code) { if (FC2_REGEX.test(code)) code = code.replace("-PPV-", " PPV "); const re = { img: "" }; const requests = [ { url: `https://blogjav.net/?s=${code}`, selectors: "#main .entry-title a", }, { url: `https://duckduckgo.com/?q=${code} site:blogjav.net`, selectors: "#links h2 a", }, { url: `https://www.google.com/search?q=${code} site:blogjav.net`, selectors: "#rso .g .yuRUbf > a", }, ]; let list = await Promise.allSettled(requests.map(item => request(item.url))); const { regex } = codeParse(code); code = code.toUpperCase(); let url = ""; for (let index = 0, { length } = requests; index < length; index++) { const { status, value } = list[index]; if (status !== "fulfilled" || !value) continue; const href = Array.from(value?.querySelectorAll(requests[index].selectors) ?? []).find(item => { const str = item.textContent.toUpperCase(); return regex.test(str) && str.includes(code); })?.href; if (!href) continue; url = href; break; } if (!url) return re; list = await Promise.allSettled([ request(url), request(`http://webcache.googleusercontent.com/search?q=cache:${url}`), ]); url = ""; for (let index = 0, { length } = list; index < length; index++) { const { status, value } = list[index]; if (status !== "fulfilled" || !value) continue; let img = value.querySelector("#main .entry-content a img"); if (!img) continue; img = img.getAttribute("data-src") ?? img.getAttribute("data-lazy-src"); if (!img) continue; url = img.replace("//t", "//img").replace("thumbs", "images"); break; } re.img = url; return re; } static async fetchJavStore(code) { if (FC2_REGEX.test(code)) code = code.replace("-PPV-", "PPV "); const re = { img: "" }; let res = await request(`https://javstore.net/search/${code}.html`); if (!res) return re; res = res.querySelectorAll("#content_news li > a"); if (!res.length) return re; const { regex } = codeParse(code); code = code.toUpperCase(); res = Array.from(res).find(item => { const str = item.title.toUpperCase(); return str.includes(code) && regex.test(str) && item.querySelector("img").src.startsWith("https"); })?.href; if (!res) return re; res = await request(res); if (!res) return re; res = res.querySelector(".news a img[alt*='.th']")?.src; if (!res?.startsWith("https")) return re; re.img = res .replace("//t", "//img") .replace("pixhost.org", "pixhost.to") .replace("thumbs", "images") .replace(".th", ""); return re; } static async fetchVideoByGuess({ code, isVR, cid }) { const re = { video: "" }; if (!code && !cid) return re; const HOST = `https://cc3001.dmm.co.jp/${isVR ? "vrsample" : "litevideo/freepv"}/`; const SUFFIX = isVR ? "vrlite" : "_dmb_w"; const list = []; const parseUrl = (prefix, num = "") => { return `${HOST}${prefix[0]}/${prefix.substring(0, 3)}/${prefix}${num}/${prefix}${num}${SUFFIX}.mp4`; }; if (code) { const [prefix, num = ""] = code.toLowerCase().split(/-|_/); const matchList = [ { prefix: ["fera", "fuga", "jrze", "mesu"], add: "h_086" }, { prefix: ["spz", "udak", "ofku"], add: "h_254" }, { prefix: ["abp", "ppt", "abw"], add: "118" }, { prefix: ["mds", "scpx"], add: "84" }, { prefix: ["shind"], add: "h_1560" }, { prefix: ["pydvr"], add: "h_1321" }, { prefix: ["meko"], add: "h_1160" }, { prefix: ["fgan"], add: "h_1440" }, { prefix: ["spro"], add: "h_1594" }, { prefix: ["hzgb"], add: "h_1100" }, { prefix: ["stcv"], add: "h_1616" }, { prefix: ["skmj"], add: "h_1324" }, { prefix: ["haru"], add: "h_687" }, { prefix: ["fone"], add: "h_491" }, { prefix: ["fsvr"], add: "h_955" }, { prefix: ["jukf"], add: "h_227" }, { prefix: ["rebd"], add: "h_346" }, { prefix: ["ktra"], add: "h_094" }, { prefix: ["supa"], add: "h_244" }, { prefix: ["zex"], add: "h_720" }, { prefix: ["pym"], add: "h_283" }, { prefix: ["hz"], add: "h_113" }, { prefix: ["dtvr"], add: "24" }, { prefix: ["umd"], add: "125" }, { prefix: ["gvg"], add: "13" }, { prefix: ["t28"], add: "55" }, { prefix: ["lol"], add: "12" }, { prefix: ["dv"], add: "53" }, ]; const add = matchList.find(item => item.prefix.includes(prefix))?.add ?? "1"; list.push(parseUrl(prefix, num)); list.push(parseUrl(`${add}${prefix}`, num)); if (num) { list.push(parseUrl(prefix, `00${num}`)); list.push(parseUrl(`${add}${prefix}`, `00${num}`)); } } if (cid) { list.push(parseUrl(cid)); list.push(parseUrl(cid.replace("00", ""))); } const res = await Promise.allSettled(unique(list).map(item => request(item, {}, "HEAD"))); for (let index = 0, { length } = res; index < length; index++) { const { status, value } = res[index]; if (status !== "fulfilled" || !value) continue; re.video = list[index]; break; } return re; } static async fetchVideoByStudio({ code, studio }) { const re = { video: "" }; if (!studio) return re; code = code.toLowerCase(); const matchList = [ { name: "東京熱", match: `https://my.cdn.tokyo-hot.com/media/samples/${REP}.mp4`, }, { name: "カリビアンコム", match: `https://smovie.caribbeancom.com/sample/movies/${REP}/720p.mp4`, }, { name: "一本道", match: `http://smovie.1pondo.tv/sample/movies/${REP}/1080p.mp4`, }, { name: "HEYZO", trans: code => code.replace(/HEYZO-/gi, ""), match: `https://sample.heyzo.com/contents/3000/${REP}/heyzo_hd_${REP}_sample.mp4`, }, { name: "天然むすめ", match: `https://smovie.10musume.com/sample/movies/${REP}/720p.mp4`, }, { name: "パコパコママ", match: `https://fms.pacopacomama.com/hls/sample/pacopacomama.com/${REP}/720p.mp4`, }, ]; let res = matchList.find(({ name }) => name === studio); if (!res) return re; res = res.match.replaceAll(REP, res.trans ? res.trans(code) : code); if (await request(res, {}, "HEAD")) re.video = res; return re; } // fetch video by guess for list static async fetchVBGFL(code) { const re = { video: "" }; code = code.replace(/HEYZO-/gi, "").toLowerCase(); const list = [ `https://my.cdn.tokyo-hot.com/media/samples/${code}.mp4`, `https://smovie.caribbeancom.com/sample/movies/${code}/720p.mp4`, `http://smovie.1pondo.tv/sample/movies/${code}/1080p.mp4`, `https://sample.heyzo.com/contents/3000/${code}/heyzo_hd_${code}_sample.mp4`, `https://smovie.10musume.com/sample/movies/${code}/720p.mp4`, `https://fms.pacopacomama.com/hls/sample/pacopacomama.com/${code}/720p.mp4`, ]; const res = await Promise.allSettled(list.map(item => request(item, {}, "HEAD"))); for (let index = 0, { length } = res; index < length; index++) { const { status, value } = res[index]; if (status !== "fulfilled" || !value) continue; re.video = list[index]; break; } return re; } // fetch video by local static async fetchVBL({ node, isVR }) { const re = { video: "" }; let res = await request(node.href); if (!res) return re; if (Domain === "JavBus") { let cid = res.querySelector("#sample-waterfall a.sample-box")?.href; cid = cid?.includes("pics.dmm.co.jp") ? cid.split("/").at(-2) : ""; if (!cid) return re; res = await this.fetchVideoByGuess({ cid, isVR }); if (res.video) re.video = res.video; } if (Domain === "JavDB") { res = res.querySelector("#preview-video source")?.getAttribute("src") ?? ""; if (res && (await request(res, {}, "HEAD"))) re.video = res; } return re; } static async fetchJavSpyl(code) { const re = { video: "" }; let res = await request(`https://api.javspyl.tk/api/?${code}`, {}, "GET", { headers: { origin: "https://javspyl.tk" }, responseType: "text", }); if (!res?.msg || res?.code) return re; res = res.msg; if (res.trim() === "no" || res.endsWith(".m3u8")) return re; res = res.includes("//") ? res : `https://${res}`; if (await request(res, {}, "HEAD")) re.video = res; return re; } static async fetchAVPreview(code) { const re = { video: "" }; let res = await request(`https://avpreview.com/zh/search?keywords=${code}`); if (!res) return re; res = Array.from(res.querySelectorAll(".container .videobox")) .find(item => code === item.querySelector("h2 strong")?.textContent) ?.querySelector("a") ?.getAttribute("href"); if (!res) return re; res = await request( "https://avpreview.com/API/v1.0/index.php", { system: "videos", action: "detail", contentid: res.split("/").at(-1), sitecode: "avpreview", ip: "", token: "", }, "GET", { responseType: "json" } ); if (!res) return re; res = res?.videos?.trailer?.replace("/hlsvideo/", "/litevideo/")?.replace("/playlist.m3u8", ""); if (!res) return re; const contentId = res.split("/").at(-1); res = [ `${res}/${contentId}_dmb_w.mp4`, `${res}/${contentId}_mhb_w.mp4`, `${res}/${contentId}_dm_w.mp4`, `${res}/${contentId}_sm_w.mp4`, ]; const list = await Promise.allSettled(res.map(item => request(item, {}, "HEAD"))); for (let index = 0, { length } = list; index < length; index++) { const { status, value } = list[index]; if (status !== "fulfilled" || !value) continue; re.video = res[index]; break; } return re; } static async fetchFC2(code) { code = code.split("-").at(-1); const HOST = "https://adult.xiaojiadianmovie.be/"; const re = { video: "" }; let res = await request(`${HOST}article/${code}/`); if (!res) return re; res = res.head .querySelector("script:last-child") .innerHTML?.match(/(?<=ae', ').*?(?=')/) ?.at(0); if (!res) return re; res = await request(`${HOST}api/v2/videos/${code}/sample`, {}, "GET", { responseType: "json", headers: { "X-FC2-Contents-Access-Token": res }, }); if (res?.path && (await request(res.path, {}, "HEAD"))) re.video = res.path; return re; } static async fetchNetflav(code) { const HOST = "https://netflav.com"; const re = { player: [] }; let res = await request(`${HOST}/search?type=title&keyword=${code}`); if (!res) return re; res = res.querySelectorAll(".grid_root .grid_cell"); if (!res.length) return re; const { regex } = codeParse(code); code = code.toUpperCase(); res = Array.from(res) .find(item => { const str = item.querySelector(".grid_title").textContent.toUpperCase(); return str.includes(code) && regex.test(str); }) ?.querySelector("a") ?.getAttribute("href"); if (!res) return re; res = await request(`${HOST}${res}`); if (!res) return re; res = res.querySelector("#__NEXT_DATA__")?.textContent; if (!res) return re; res = JSON.parse(res)?.props?.initialState?.video?.data?.srcs; if (!res?.length) return re; const matchList = [ { regex: /\/\/(mm9842\.com|www\.avple\.video|asianclub\.tv)/, parse: async url => { const [protocol, href] = url.split("//"); const [host, ...pathname] = href.split("/"); const res = await request( `${protocol}//${host}/api/source/${pathname.pop()}`, { r: "", d: host }, "POST" ); if (!res?.success) return []; return (res?.data ?? []).map(({ file, label = "320p", type }) => { return { src: file, title: label, type: `video/${type}` }; }); }, }, { regex: /\/\/(embedgram\.com|vidoza\.net)/, parse: async url => { const res = await request(url); if (!res) return []; return Array.from(res?.querySelectorAll("video source") ?? []).map( ({ src, title = "320p", type }) => { return { src, title, type }; } ); }, }, ]; res = await Promise.allSettled(res.map(url => matchList.find(({ regex }) => regex.test(url))?.parse(url))); for (let index = 0, { length } = res; index < length; index++) { const { status, value } = res[index]; if (status === "fulfilled" && value?.length) re.player = re.player.concat(value); } return re; } static async fetchTranslate(str) { const re = { title: "" }; const res = await request( "https://www.google.com/async/translate?vet=12ahUKEwixq63V3Kn3AhUCJUQIHdMJDpkQqDh6BAgCECw..i&ei=CbNjYvGCPYLKkPIP05O4yAk&yv=3", { async: `translate,sl:auto,tl:zh-CN,st:${encodeURIComponent( str.trim() )},id:1650701080679,qc:true,ac:false,_id:tw-async-translate,_pms:s,_fmt:pc`, }, "POST", { responseType: "" } ); if (res) re.title = res?.querySelector("#tw-answ-target-text")?.textContent ?? ""; return re; } static async _fetchMagnet(code, { site, host, search, selectors, filter }) { const key = `${site.substring(0, 3).toLowerCase()}_magnet`; const re = { [key]: [] }; let res = await request(`${host}${search}`); if (!res) return re; res = res.querySelectorAll(selectors); if (!res.length) return re; const { regex } = codeParse(code); for (const item of res) { const name = (filter.name(item) ?? "").replace(/[\u200B-\u200D\uFEFF]/g, ""); if (!regex.test(name)) continue; const size = filter.size(item); let href = filter.href(item); if (href && !href.includes("//")) href = `${host}${href.replace(/^\//, "")}`; re[key].push({ name, zh: ZH_REGEX.test(name), link: filter.link(item).split("&")[0], size, bytes: transToBytes(size), date: filter.date(item), href, from: site, }); } return re; } static fetchSukebei(code) { return this._fetchMagnet(code, { site: "Sukebei", host: "https://sukebei.nyaa.si/", search: `?f=0&c=0_0&q=${code}`, selectors: ".table-responsive table tbody tr", filter: { name: e => e.querySelector("td:nth-child(2) a").textContent, link: e => e.querySelector("td:nth-child(3) a:last-child").href, size: e => e.querySelector("td:nth-child(4)").textContent, date: e => e.querySelector("td:nth-child(5)").textContent.split(" ")[0], href: e => e.querySelector("td:nth-child(2) a").getAttribute("href"), }, }); } static fetchBTSOW(code) { return this._fetchMagnet(code, { site: "BTSOW", host: "https://btsow.com/", search: `search/${code}`, selectors: ".data-list .row:not(.hidden-xs)", filter: { name: e => e.querySelector(".file").textContent, link: e => `magnet:?xt=urn:btih:${e.querySelector("a").href.split("/").pop()}`, size: e => e.querySelector(".size").textContent, date: e => e.querySelector(".date").textContent, href: e => e.querySelector("a").getAttribute("href"), }, }); } static fetchBTDigg(code, index = 0) { return this._fetchMagnet(code, { site: "BTDigg", host: `https://${index ? "www." : ""}btdig.com/`, search: `search?order=0&q=${code}`, selectors: ".one_result", filter: { name: e => e.querySelector(".torrent_name").textContent, link: e => e.querySelector(".torrent_magnet a").href, size: e => e.querySelector(".torrent_size").textContent, date: e => e.querySelector(".torrent_age").textContent, href: e => e.querySelector(".torrent_name a").href, }, }); } static async fetchMGS({ code, title }) { const HOST = "https://www.mgstage.com"; const re = { mgs_score: [], video: "" }; let res = await request(`${HOST}/search/cSearch.php?search_word=${title}&list_cnt=120`); if (!res) return re; res = res.querySelectorAll(".rank_list li"); if (!res.length) return re; const { regex } = codeParse(code); code = code.toUpperCase(); res = Array.from(res).find(item => { const str = item.querySelector("a").getAttribute("href").toUpperCase(); return str.includes(code) && regex.test(str); }); if (!res) return re; let [score, video] = await Promise.allSettled([ request(`${HOST}${res.querySelector("a").getAttribute("href")}`), request( `${HOST}${res .querySelector(".sample_movie_btn a") .getAttribute("href") .replace("player.html/", "Respons.php?pid=")}`, {}, "GET", { responseType: "json" } ), ]); if (score.status === "fulfilled" && score.value) { score = score.value.querySelector(".detail_data .review")?.textContent?.match(NUM_REGEX); if (score?.length) re.mgs_score.push({ score: score[0], total: 5, num: score[1], from: "MGS" }); } if (video.status === "fulfilled" && video.value) { video = video.value?.url.split("?")[0].replace("ism/request", "mp4"); if (await request(video, {}, "HEAD")) re.video = video; } return re; } static async fetchLib(code) { const HOST = "https://www.javlibrary.com/cn/"; const re = { lib_score: [], star: [] }; let res = await request(`${HOST}vl_searchbyid.php?keyword=${code}`); if (!res) return re; if (res.querySelector(".videothumblist")) { res = res.querySelectorAll(".videothumblist .videos .video > a"); if (!res.length) return re; const { regex } = codeParse(code); code = code.toUpperCase(); res = Array.from(res).find(({ title }) => { const str = title.toUpperCase(); return str.includes(code) && regex.test(str); }); if (!res) return re; res = await request(`${HOST}${res.getAttribute("href")}`); } const score = res.querySelector("#video_review .text .score")?.textContent?.replace(/\(|\)/g, "") ?? ""; if (score) re.lib_score.push({ score, total: 10, from: "JavLibrary" }); re.star = Array.from(res.querySelectorAll("#video_cast .text .star")).map(item => item.textContent); return re; } static async fetchDMM({ code, title }) { const re = { fetchVideo: "", fetchInfo: "" }; let res = await request(`https://jav.land/tw/id_search.php?keys=${code.toUpperCase()}`); if (!res) return re; res = res.querySelector("table.videotextlist tbody tr"); if (!res) return re; res = res.querySelectorAll("td"); if (!res?.length || res[0].textContent !== "內容 ID:") return re; const cid = res[1].textContent; re.fetchVideo = async () => { const result = { video: "" }; const first = cid[0]; const second = cid.substring(0, 3); const list = [ `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_dmb_w.mp4`, `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_mhb_w.mp4`, `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_dm_w.mp4`, `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_sm_w.mp4`, `https://cc3001.dmm.co.jp/vrsample/${first}/${second}/${cid}/${cid}vrlite.mp4`, ]; const res = await Promise.allSettled(list.map(item => request(item, {}, "HEAD"))); for (let index = 0, { length } = res; index < length; index++) { const { status, value } = res[index]; if (status !== "fulfilled" || !value) continue; result.video = list[index]; break; } return result; }; re.fetchInfo = async () => { const result = { dmm_score: [], star: [], video: "" }; const res = await request(`https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=${cid}/`, {}, "GET", { headers: { "accept-language": "ja-JP" }, }); if (!res) return result; let params = res.head.querySelector("script[type='application/ld+json']")?.textContent; if (!params) return result; params = JSON.parse(params); if (!params?.name || (title && !title.includes(params.name))) return result; if (params.aggregateRating) { result.dmm_score.push({ score: params.aggregateRating.ratingValue, total: 5, num: params.aggregateRating.ratingCount, from: "DMM", }); } result.star = Array.from(res.querySelectorAll("#performer a")).map(item => item.textContent); const video = params.subjectOf?.contentUrl; if (video && (await request(video, {}, "HEAD"))) result.video = video; return result; }; return re; } static async fetchDB(code) { const HOST = "https://javdb.com"; const re = { db_score: [], star: [], video: "", sub: [] }; let res = await request(`${HOST}/search?q=${code}&sb=0`); if (!res) return re; res = res.querySelectorAll(".movie-list .item a"); if (!res.length) return re; const { regex } = codeParse(code); code = code.toUpperCase(); res = Array.from(res).find(item => { const str = item.querySelector("strong").textContent.toUpperCase(); return str.includes(code) && regex.test(str); }); if (!res) return re; const href = `${HOST}${res.getAttribute("href")}`; res = await request(href); if (!res) return re; const scoreReg = /評分|Rating/; const starReg = /演員|Actor/; for (const item of res.querySelectorAll(".movie-panel-info .panel-block")) { let [label, value] = item.querySelectorAll("strong, .value"); if (!label || !value) continue; label = label.textContent; value = value.textContent; if (scoreReg.test(label)) { const [score, num] = value.match(NUM_REGEX); re.db_score.push({ score, total: 5, num, from: "JavDB" }); continue; } if (!starReg.test(label)) continue; re.star = value .split("\n") .filter(item => item.includes("♀")) .map(item => item.replace("♀", "").trim()); } if (res.querySelector("a.preview-video-container")) { const video = res.querySelector("#preview-video source")?.src || ""; if (video && (await request(video, {}, "HEAD"))) re.video = video; } for (const item of res.querySelectorAll("#magnets-content .item")) { const first = item.querySelector("a"); if (!first) continue; const size = first.querySelector(".meta")?.textContent?.trim() ?? ""; re.sub.push({ from: "JavDB", href, name: first.querySelector(".name").textContent, link: first.href.split("&")[0], size, bytes: transToBytes(size), zh: !!first.querySelector(".tag.is-warning.is-small.is-light"), date: item.querySelector(".time")?.textContent ?? "", }); } return re; } static async _driveSearch(params, keys = []) { if (typeof params === "string") params = { search_value: params }; const res = await request( "https://webapi.115.com/files/search", { offset: 0, limit: 10000, date: "", aid: 1, cid: 0, pick_code: "", type: 4, source: "", format: "json", o: "user_ptime", asc: 0, star: "", suffix: "", ...params, }, "GET", { responseType: "json" } ); if (!res) return; if (!res.state) return notify({ title: res.error }); return res.data.map(({ n, pc, fid, cid, ...item }) => { const _item = { n, pc, fid, cid }; for (const key of keys) _item[key] = item[key]; return _item; }); } static async _driveCid(name, type) { let res = await this.driveFile(); if (!res) return; if (!res.state) return notify({ title: res?.errNo === 20130827 ? "文件排序不支持" : res.error }); res = res.data.find(({ n }) => n === name)?.cid ?? ""; if (res || type === "find") return res; res = await request("https://webapi.115.com/files/add", { pid: 0, cname: name }, "POST"); if (!res) return; if (!res.state) return notify({ title: res.error }); return res?.cid ?? ""; } static driveFile(params = {}) { if (typeof params === "string") params = { cid: params }; return request( "https://webapi.115.com/files", { aid: 1, cid: 0, o: "user_ptime", asc: 0, offset: 0, show_dir: 1, limit: 115, code: "", scid: "", snap: 0, natsort: 1, record_open_time: 1, source: "", format: "json", ...params, }, "GET", { responseType: "json" } ); } static async driveVideo(cid, keys = []) { if (!cid) return; const res = await this.driveFile({ cid, custom_order: 0, star: "", suffix: "", type: 4 }); if (!res?.data) return; return res.data.map(({ n, pc, fid, cid, t, ...item }) => { const _item = { n, pc, fid, cid, t }; for (const key of keys) _item[key] = item[key]; return _item; }); } static async driveSign() { const res = await request( "http://115.com/", { ct: "offline", ac: "space", _: new Date().getTime() }, "GET", { responseType: "json" } ); if (res?.state) return { sign: res.sign, time: res.time }; } static driveAddTask({ url, wp_path_id, sign, time }) { return request( "https://115.com/web/lixian/?ct=lixian&ac=add_task_url", { url, wp_path_id, sign, time }, "POST" ); } static driveRename(res) { const data = {}; for (const { fid, file_name } of res) data[`files_new_name[${fid}]`] = file_name; return request("https://webapi.115.com/files/batch_rename", data, "POST"); } static driveClear({ pid, fids }) { const data = { pid, ignore_warn: 1 }; fids.forEach(({ fid, cid }, index) => { data[`fid[${index}]`] = fid ?? cid; }); return request("https://webapi.115.com/rb/delete", data, "POST"); } static driveInitUpload(params) { return request("https://uplb.115.com/3.0/sampleinitupload.php", { userid: "", ...params }, "POST"); } static driveUpload({ host, object, policy, accessid, callback, signature }, blob, name) { const formdata = new FormData(); formdata.append("file", blob, name); formdata.append("signature", signature); formdata.append("callback", callback); formdata.append("success_action_status", 200); formdata.append("OSSAccessKeyId", accessid); formdata.append("policy", policy); formdata.append("key", object); formdata.append("name", name); return GM_xmlhttpRequest({ method: "POST", url: host, data: formdata }); } static async driveLabel() { const res = await request("https://webapi.115.com/label/list?keyword=&limit=115", {}, "GET", { responseType: "json", }); if (res?.state) return res.data.list; } static driveTag({ file_ids, file_label }) { return request("https://webapi.115.com/files/batch_label", { file_ids, file_label, action: "add" }, "POST"); } static driveDetail(fid) { return request(`https://webapi.115.com/category/get?cid=${fid}`, {}, "GET", { responseType: "json", }); } } class Common { menus = [ { title: "站点设置", key: "global", prefix: "G", options: [ { name: "暗黑模式", key: "DARK", type: "switch", info: "常用页面暗黑模式", defaultVal: window.matchMedia("(prefers-color-scheme: dark)").matches, hotkey: "d", }, { name: "快捷搜索", key: "SEARCH", type: "switch", info: "快捷键 <kbd>/</kbd> 获取搜索框焦点,<kbd>Ctrl</kbd> + <kbd>/</kbd> 快速搜索粘贴板首项", defaultVal: true, hotkey: "k", }, { name: "点击事件", key: "CLICK", type: "switch", info: "<code>影片</code> / <code>演员</code> / <code>站点跳转</code> 点击新窗口 (左键前台,右键后台) 打开", defaultVal: true, hotkey: "c", }, ], }, { title: "列表页设置", key: "list", prefix: "L", options: [ { name: "预览替换", key: "MIT", type: "switch", info: "影片预览图替换为封面图", defaultVal: true, }, { name: "悬停预览", key: "MV", type: "number", info: '设置封面悬停时长 (ms) 以预览视频,建议 300+,0 禁用预览<br>仅 <strong>预览替换</strong> / <strong>(JavDB)大封面</strong> 模式下生效,获取自 <a href="https://javspyl.tk/" class="link-primary">JavSpyl</a> / <a href="https://avpreview.com/zh/" class="link-primary">AVPreview</a> / <a href="https://adult.xiaojiadianmovie.be/" class="link-primary">FC2</a>', placeholder: "仅支持整数 ≥ 0", defaultVal: 300, }, { name: "标题等高", key: "MTH", type: "switch", info: "影片标题强制等高", defaultVal: true, }, { name: "标题最大行", key: "MTL", type: "number", info: "影片标题最大显示行数,超出省略。0 不限制 (等高模式下最小有效值 1)", placeholder: "仅支持整数 ≥ 0", defaultVal: 2, }, { name: "滚动加载", key: "SCROLL", type: "switch", info: "滚动加载下一页", defaultVal: true, }, { name: "合并列表", key: "MERGE", type: "textarea", info: "列表名不可重复,同一列表内地址不可重复 (仅支持影片列表相对地址)<br>合并列表每页按 <code>影片评分</code> (如有) > <code>影片日期</code> > <code>填写顺序</code> 综合排序,并自动去重<br>多列表需以空行分隔", placeholder: "[列表1]\n/genre/28#连裤袜\n/star/two#星宮一花\n\n[列表2]\n/\n/uncensored", defaultVal: "", }, { name: "影片筛选", key: "FILTER", type: "textarea", info: '支持 <code>[code]</code> / <code>[title]</code> / <code>[score]</code> / <code>[tags]</code> 分组的 <code>屏蔽</code> (优先) & <code>高亮</code> 规则设置<br>规则类型默认为屏蔽规则,高亮需以 <code>^</code> 开头标识<br>规则语法默认全字符查找,支持追加参数:<code>#start</code> 仅匹配开头,<code>#end</code> 仅匹配结尾<br>规则语法兼容 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions" class="link-primary">正则</a> (不同时支持追加参数),格式如 <code>reg:/^stars-/i</code><br><code>[score]</code> 仅 <strong>JavDB</strong>,提取格式如 <code>5.0分, 由6人評價</code><br><code>[tags]</code> 提取格式如 <code>高清,字幕,3天前新種</code><br>格式参考『<strong>合并列表</strong>』', placeholder: "[title]\n排泄\n失禁\n\n[code]\n^SSIS-#start\nIPX#start", defaultVal: "", }, ], }, { title: "详情页设置", key: "movie", prefix: "M", options: [ { name: "在线资源", key: "RES", type: "input", info: '自定义资源配置,支持参数:<br><code>#img</code> 预览大图,<code>#video</code> 视频预览,<code>#player</code> 在线视频<br>获取自 <a href="https://blogjav.net/" class="link-primary">BlogJav</a> / <a href="https://javstore.net/" class="link-primary">JavStore</a>,<a href="https://javspyl.tk/" class="link-primary">JavSpyl</a> / <a href="https://avpreview.com/zh/" class="link-primary">AVPreview</a> / <a href="https://adult.xiaojiadianmovie.be/" class="link-primary">FC2</a> / <a href="https://www.mgstage.com/" class="link-primary">MGS</a> / <a href="https://www.dmm.co.jp/" class="link-primary">DMM</a> / <a href="https://javdb.com/" class="link-primary">JavDB</a>,<a href="https://netflav.com/" class="link-primary">Netflav</a>', placeholder: "为空时展示页面默认", defaultVal: "#video#img#player", }, { name: "快捷复制", key: "COPY", type: "switch", info: "为 <code>标题</code> / <code>番号</code> / <code>演员</code> (右键) / <code>磁链</code> 提供快捷复制", defaultVal: true, }, { name: "标题机翻", key: "TITLE", type: "switch", info: '翻自 <a href="https://translate.google.com/" class="link-primary">Google</a>', defaultVal: true, }, { name: "演员匹配", key: "STAR", type: "switch", info: '如无,获取自 <a href="https://www.javlibrary.com/cn/" class="link-primary">JavLibrary</a> / <a href="https://www.dmm.co.jp/" class="link-primary">DMM</a> / <a href="https://javdb.com/" class="link-primary">JavDB</a>', defaultVal: true, }, { name: "影片评分", key: "SCORE", type: "input", info: '自定义评分配置,支持参数:<br><code>#db</code> 获取自 <a href="https://javdb.com/" class="link-primary">JavDB</a>,<code>#lib</code> 获取自 <a href="https://www.javlibrary.com/cn/" class="link-primary">JavLibrary</a>,<code>#dmm</code> 获取自 <a href="https://www.dmm.co.jp/" class="link-primary">DMM</a>,<code>#mgs</code> 获取自 <a href="https://www.mgstage.com/" class="link-primary">MGS</a>', placeholder: "为空时展示页面默认", defaultVal: "#db#lib#dmm", }, { name: "站点跳转", key: "JUMP", type: "textarea", info: `番号关键词以 <code>${REP}</code> 表示<br>支持追加参数:<code>#query</code> 预查询资源及字幕,<code>#jump</code> 跳转后自动匹配首项<br>格式参考『<strong>合并列表</strong>』`, placeholder: `[数据库]\nhttps://javdb.com/search?q=${REP}&f=all#jump\nhttps://www.javlibrary.com/cn/vl_searchbyid.php?keyword=${REP}\n\n[在线视频]\nhttps://www3.bestjavporn.com/search/${REP}/#query#jump\nhttps://jable.tv/search/${REP}/#query#jump\n\n[磁力链接]\nhttps://btdig.com/search?q=${REP}#query\nhttps://idope.se/torrent-list/${REP}/#query`, defaultVal: `[跳转]\nhttps://www.JavLibrary.com/cn/vl_searchbyid.php?keyword=${REP}#query#jump\nhttps://${ Domain === "JavDB" ? `www.JavBus.com/search/${REP}` : `JavDB.com/search?f=all&q=${REP}&sb=0` }#query#jump`, }, { name: "磁链最大行", key: "LINE", type: "number", info: "磁力链接最大显示行数,超出以滚动显示。0 不限制", placeholder: "仅支持整数 ≥ 0", defaultVal: 10, }, { name: "字幕筛选", key: "SUB", type: "switch", info: '替换为 <a href="https://javdb.com/" class="link-primary">JavDB</a> 磁链数据', defaultVal: false, }, { name: "磁链排序", key: "SORT", type: "switch", info: "综合排序 <code>字幕</code> > <code>大小</code> > <code>日期</code>", defaultVal: true, }, { name: "磁链搜索", key: "MAGNET", type: "input", info: '自定义搜索配置并自动去重,支持参数:<br><code>#suk</code> 获取自 <a href="https://sukebei.nyaa.si/" class="link-primary">Sukebei</a>,<code>#bts</code> 获取自 <a href="https://btsow.com/" class="link-primary">BTSOW</a>,<code>#btd</code> 获取自 <a href="https://btdig.com/" class="link-primary">BTDigg</a>', placeholder: "为空时展示页面默认", defaultVal: "#btd#bts#suk", }, ], }, { title: "115 相关", key: "drive", prefix: "D", options: [ { name: "网盘资源", key: "MATCH", type: "switch", info: '<strong>需要 <a href="https://115.com/" class="link-primary">网盘</a> 已登录,并调整文件排序方式为 <code>修改时间</code></strong><br>列表页:网盘资源匹配<br>详情页:网盘资源匹配 & (一键) 离线按钮开关<br>(完整体验建议允许弹窗通知权限)', defaultVal: true, }, { name: "列表离线", key: "LOL", type: "switch", info: "影片列表支持简易『<strong>一键离线</strong>』执行动作:<br>『<strong>磁链搜索</strong>』『<strong>磁链排序</strong>』『<strong>离线验证</strong>』『<strong>最大失败数</strong>』『<strong>离线清理</strong>』『<strong>文件重命名</strong>』", defaultVal: true, }, { name: "下载目录", key: "CID", type: "input", info: "设置离线下载目录 <strong>cid</strong> 或 <strong>动态目录</strong> (未找到时动态创建),建议直接填写 <strong>cid</strong> 效率更高<br><strong>动态目录</strong> 支持填写 <strong>目录名称</strong> 和 <strong>动态参数</strong>:<br><code>${#star}</code> (首位) 女演员<br><code>${#series}</code> 系列<br><code>${#studio}</code> 制造商 / 片商 / 卖家<br><strong>目录名称</strong> & <strong>动态参数</strong> 可组合使用,如 <code>${#series/#star/云下载}</code>", placeholder: "cid 或 动态目录", defaultVal: "${云下载}", }, { name: "离线验证", key: "VERIFY", type: "number", info: "添加离线后执行,查询以验证离线下载结果,每次间隔一秒<br>设置验证次数上限,上限次数越多验证越精准<br>建议 3 ~ 5,默认 5", placeholder: "仅支持整数 ≥ 0", defaultVal: 5, }, { name: "最大失败数", key: "FAIL", type: "number", info: "『<strong>一键离线</strong>』时累计验证失败的磁链数,最大值时终止本次任务。0 不限制", placeholder: "仅支持整数 ≥ 0", defaultVal: 5, }, { name: "离线清理", key: "CLEAR", type: "switch", info: "『<strong>离线验证</strong>』成功后执行,匹配番号以清理无关文件", defaultVal: true, }, { name: "文件重命名", key: "RENAME", type: "input", info: '『<strong>离线验证</strong>』成功后执行,支持参数:<br><code>${字幕}</code> "【中文字幕】",非字幕资源则为空<br><code>${番号}</code> 页面番号 (番号为必须值,如重命名未包含将自动追加前缀)<br><code>${标题}</code> 页面标题,已去除番号<br><code>${序号}</code> 仅作用于视频文件,数字 1 起', placeholder: "勿填写后缀,可能导致资源不可用", defaultVal: "${字幕}${番号} ${标题}", }, { name: "自动打标", key: "TAG", type: "input", info: "『<strong>离线验证</strong>』成功后执行,根据设置值匹配网盘已有标签自动打标,支持参数:<br><code>#genre</code> 类别,<code>#star</code> 演员", placeholder: "如 #genre#star", defaultVal: "#genre#star", }, { name: "图片上传", key: "UPLOAD", type: "input", info: "『<strong>离线验证</strong>』成功后执行 (弹窗通知),支持参数:<br><code>#cover</code> 封面图,<code>#img</code> 预览大图 (通常较大,确保网络通畅并耐心等待上传)", placeholder: "如 #cover#img", defaultVal: "#cover#img", }, { name: "资源调整", key: "MODIFY", type: "input", info: "针对详情页网盘匹配资源的自定义调整配置,支持参数 (顺序不分先后):<br><code>#delete</code> 删除对应视频文件,并忽略其他参数<br><code>#rename</code> 详见『<strong>文件重命名</strong>』<br><code>#upload</code> 详见『<strong>图片上传</strong>』<br><code>#clear</code> 详见『<strong>离线清理</strong>』<br><code>#tag</code> 详见『<strong>自动打标</strong>』", placeholder: "如 #rename#clear#tag#upload", defaultVal: "#rename#clear#tag#upload", }, ], }, { title: "进阶", key: "advanced", prefix: "A", options: [ { name: "资源跳转", key: "LINK", type: "textarea", info: "自定义网盘资源跳转链接,兼容列表及详情页,仅支持分组 <code>[left]</code> / <code>[right]</code>,链接参数:<br><code>${#pickcode}</code> 可用于跳转播放<br><code>${#cid}</code> 可用于跳转目录<br><code>${#path}</code> 资源路径<br><code>${#name}</code> 资源名称<br>格式参考『<strong>合并列表</strong>』", placeholder: "[left]\nhttps://v.anxia.com/?pickcode=${#pickcode}\n\n[right]\nhttps://115.com/?cid=${#cid}&offset=0&mode=wangpan", defaultVal: "", }, ], }, ]; style = { custom: ` :root { --x-bgc: #121212; --x-sub-bgc: #202020; --x-ftc: #fffffff2; --x-sub-ftc: #aaa; --x-grey: #313131; --x-blue: #0a84ff; --x-orange: #ff9f0a; --x-green: #30d158; --x-red: #ff453a; --x-yellow: #ffd60a; --x-line-h: 22px; --x-thumb-w: 190px; --x-cover-w: 360px; --x-thumb-ratio: 147 / 200; --x-cover-ratio: 400 / 269; --x-avatar-ratio: 1; --x-sprite-ratio: 4 / 3; } .x-hide, .x-scrollbar-hide body::-webkit-scrollbar { display: none !important; } .x-show { display: block !important; } #x-mask { position: fixed; top: 0; left: 0; z-index: 9999; display: none; box-sizing: border-box; width: 100vw; height: 100vh; margin: 0; padding: 0; background: transparent; border: none; } .x-in { opacity: 1 !important; transition: opacity .25s linear !important; } .x-cover { width: var(--x-cover-w) !important; } .x-cover > :first-child { aspect-ratio: var(--x-cover-ratio) !important; } .x-cover img { object-fit: contain !important; } .x-ellipsis { display: -webkit-box !important; overflow: hidden; white-space: unset !important; text-overflow: ellipsis; -webkit-line-clamp: 1; -webkit-box-orient: vertical; word-break: break-all; } .x-line { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } #x-status { margin-bottom: 20px; color: var(--x-sub-ftc); font-size: 14px !important; text-align: center; } .x-highlight { box-shadow: 0 0 0 4px var(--x-red) !important; } .x-ml { margin-left: 10px; } .x-side { position: absolute; top: 50%; z-index: 7; display: flex; align-items: center; justify-content: center; width: 60px; height: 60px; color: var(--x-sub-bgc); font-size: 30px; background: #fff; border-radius: 50%; transform: translateY(-50%); cursor: pointer; opacity: .4; transition: opacity .2s linear; user-select: none; } .x-side:hover { opacity: .8; } .x-side > * { top: 0; } .x-side.prev { left: 30px; } .x-side.next { right: 30px; } .x-flex-center { display: flex !important; align-items: center; } .x-grid { display: grid !important; } *:has(> .x-jump) { margin-right: -5px !important; } .x-jump { min-width: 44px; margin: 0 5px 5px 0 !important; cursor: pointer; } .x-title { line-height: var(--x-line-h) !important; } *:has(> .x-player) .x-title { color: var(--x-blue) !important; font-weight: bold; } .x-preview, .x-loading *:has(> img), .x-player { position: relative; display: block; overflow: hidden; } .x-loading *:has(> img)::before, .x-player::before, .x-player::after { position: absolute; top: 50%; left: 50%; cursor: pointer; opacity: .8; content: ""; } .x-loading *:has(> img):not(.x-player)::before, .x-player::before { z-index: 8; width: 60px; height: 60px; background: #0a5ee0; border-radius: 50%; filter: drop-shadow(2px 5px 10px rgb(0 0 0 / 50%)); translate: -50% -50%; } .x-loading *:has(> img)::before, .x-zh.x-player::before { border: 6px solid var(--x-yellow); } .x-player::after { z-index: 9; border-top: 15px solid transparent; border-bottom: 15px solid transparent; border-left: 30px solid #fff; transform: translate(-40%, -50%); } .x-loading *:has(> img)::before { border-color: transparent; border-bottom-color: var(--x-orange) !important; animation: rotation 1s linear infinite; } .x-loading *:has(> img):not(.x-player)::before { background: transparent; border-color: #000c; } .x-loading.success *:has(> img)::before { border-color: var(--x-green) !important; } .x-loading.timeout *:has(> img)::before { border-color: var(--x-orange) !important; } .x-loading.fail *:has(> img)::before { border-color: var(--x-red) !important; } @keyframes rotation { 0% { rotate: 0deg; } 100% { rotate: 360deg; } } .x-preview :is(img, video) { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; } .x-preview video { z-index: 10; background-color: inherit; opacity: 0; transition: opacity .25s ease-in !important; } .x-preview video::-webkit-media-controls-fullscreen-button { display: none; } .x-btn { display: none; margin-right: 4px; padding: 1px 4px; color: #fff !important; font-weight: normal; font-size: calc(1em - 2px); background: var(--x-blue); border-radius: 2px; cursor: pointer; } .x-btn::after { content: "调整"; } .x-btn.active { cursor: wait; opacity: .8; } .x-btn.active::after { content: "请求中..."; } .x-btn.pending::after { content: "校验中..."; } `, common: "::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:var(--x-sub-ftc,#aaa);border-radius:4px}*{text-decoration:none!important;text-shadow:none!important;outline:0!important}", dark: "::-webkit-scrollbar-thumb,button{background:var(--x-grey)!important}*{box-shadow:none!important}:not(span[class]){border-color:var(--x-grey)!important}body,html,input{background:var(--x-bgc)!important}::placeholder,body{color:var(--x-sub-ftc)!important}nav{background:var(--x-sub-bgc)!important}a,button,h3,h4,input,p{color:var(--x-ftc)!important}img,video{filter:brightness(.9) contrast(.9)!important}", }; init() { Store.init(); const key = Object.keys(this.routes).find(key => this.routes[key].test(location.pathname)); this.registerMenu(key); return { ...this, ...this[key] }; } registerMenu(active) { const exclude = this.excludeMenu ?? []; const menus = this.menus.filter(item => !exclude.includes(`${item.prefix}_`)); const { length } = menus; if (!length) return; let tabStr = ""; let panelStr = ""; const commands = []; if (!menus.some(item => item.key === active)) active = menus[0].key; for (let index = 0; index < length; index++) { const { title, key, prefix, options } = menus[index]; let sections = ""; for (let idx = 0, len = options.length; idx < len; idx++) { let { name, key: curKey, type, info, placeholder = "", defaultVal, hotkey = "" } = options[idx]; curKey = `${prefix}_${curKey}`; if (exclude.includes(curKey)) continue; commands.push(curKey); const uniKey = `${Domain}_${curKey}`; const val = GM_getValue(uniKey, defaultVal); this[curKey] = val; let section = ""; if (type === "switch") { if (hotkey && hotkey !== "s") { GM_registerMenuCommand( `${val ? "关闭" : "开启"}${name}`, () => { GM_setValue(uniKey, !val); location.reload(); }, hotkey ); } section = ` <div class="form-check form-switch"> <input type="checkbox" class="form-check-input" role="switch" id="${curKey}" aria-describedby="${curKey}_Help" ${val ? "checked" : ""} name="${curKey}" > <label class="form-check-label" for="${curKey}">${name}</label> </div> `; } else if (type === "textarea") { section = ` <label class="form-label" for="${curKey}">${name}</label> <textarea rows="5" class="form-control" id="${curKey}" aria-describedby="${curKey}_Help" placeholder="${placeholder}" name="${curKey}" >${val ?? ""}</textarea> `; } else { section = ` <label class="form-label" for="${curKey}">${name}</label> <input type="${type}" class="form-control" id="${curKey}" aria-describedby="${curKey}_Help" value="${val ?? ""}" placeholder="${placeholder}" name="${curKey}" > `; } if (info) section += `<div id="${curKey}_Help" class="form-text">${info}</div>`; sections += `<div class="mb-3">${section}</div>`; } if (!sections) continue; const isActive = key === active; tabStr += ` <button class="nav-link${isActive ? " active" : ""} text-start" id="${key}-tab" data-bs-toggle="pill" data-bs-target="#${key}" type="button" role="tab" aria-controls="${key}" aria-selected="${isActive}" >${title}</button> `; panelStr += ` <div class="tab-pane fade${isActive ? " show active" : ""}" id="${key}" role="tabpanel" aria-labelledby="${key}-tab" tabindex="0" > ${sections} </div> `; } if (!tabStr || !panelStr) return; GM_addStyle(this.style.custom); DOC.addEventListener( "XContentLoaded", () => { const title = `${GM_info.script.name} v${GM_info.script.version}`; DOC.body.insertAdjacentHTML( "beforeend", `<iframe id="x-mask" src="about:blank" title="${title}"></iframe>` ); const iframe = DOC.querySelector("#x-mask"); const _DOC = iframe.contentWindow.document; const { body } = _DOC; _DOC.head.insertAdjacentHTML( "beforeend", `<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous"><style>${this.style.common}*{overscroll-behavior-y:contain}</style><base target="_blank">` ); GM_addElement(body, "script", { src: "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js", integrity: "sha384-IDwe1+LCz02ROU9k972gdyvl+AESN10+x7tBKgc9I5HFtuNz0wWnPclzo6p9vxnk", crossorigin: "anonymous", }); body.classList.add("bg-transparent"); body.insertAdjacentHTML( "afterbegin", `<button id="openControlPanel" type="button" class="d-none" data-bs-toggle="modal" data-bs-target="#controlPanel" >openControlPanel</button> <div class="modal fade" id="controlPanel" tabindex="-1" aria-labelledby="controlPanelLabel" aria-hidden="true" > <div class="modal-dialog modal-lg modal-fullscreen-lg-down modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title fs-5" id="controlPanelLabel">控制面板 - <a href="https://sleazyfork.org/zh-CN/scripts/435360-javscript" class="link-secondary" >${title}</a> </h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" ></button> </div> <div class="modal-body"> <form class="mb-0"> <div class="d-flex align-items-start"> <div class="nav flex-column nav-pills me-4 sticky-top" id="v-pills-tab" role="tablist" aria-orientation="vertical" > ${tabStr} </div> <div class="tab-content flex-fill" id="v-pills-tabContent"> ${panelStr} </div> </div> </form> </div> <div class="modal-footer"> <div class="flex-fill"> <div class="btn-group" role="group" aria-label="Import and Export"> <button type="button" class="btn btn-link" data-action="import">导入</button> <button type="button" class="btn btn-link" data-action="export">导出</button> </div> </div> <button type="button" class="btn btn-danger" data-bs-dismiss="modal" data-action="restart" >重置脚本</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-action="clear" >清除缓存</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-action="reset" >默认设置</button> <button type="button" class="btn btn-primary" data-bs-dismiss="modal" data-action="save" >保存设置</button> </div> </div> </div> </div>` ); body.querySelector("#controlPanel .modal-footer").addEventListener("click", e => { const { action } = e.target.dataset; if (!action) return; e.preventDefault(); e.stopPropagation(); const prefixes = MatchDomains.slice(0, -1).map(item => item.domain); if (action === "import") { const upConfig = file => { const fileReader = new FileReader(); fileReader.readAsText(file); fileReader.onload = () => { for (const [key, val] of Object.entries(JSON.parse(fileReader.result))) { if (prefixes.includes(key.split("_")[0])) GM_setValue(key, val); } location.reload(); }; }; const upload = DOC.create("input", { type: "file", accept: ".json" }); upload.addEventListener( "change", e => { const file = e.target.files[0]; if (file?.type !== "application/json") return; if (file.name.split(".json")[0] === title) return upConfig(file); if (window.confirm("导入配置可能与当前版本不符,是否继续?")) return upConfig(file); }, false ); return upload.click(); } if (action === "export") { const config = {}; GM_listValues().forEach(key => { if (prefixes.includes(key.split("_")[0])) config[key] = GM_getValue(key); }); const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" }); const download = DOC.create("a", { download: `${title}.json`, href: URL.createObjectURL(blob), }); return download.click(); } if (action === "save") { const data = Object.fromEntries( new FormData(body.querySelector("#controlPanel form")).entries() ); commands.forEach(key => GM_setValue(`${Domain}_${key}`, data[key] ?? "")); } if (action === "reset") { GM_listValues().forEach(name => name.startsWith(Domain) && GM_deleteValue(name)); } if (action === "clear") { GM_setValue("DETAILS", {}); GM_setValue("RESOURCE", {}); localStorage.clear(); } if (action === "restart") GM_listValues().forEach(name => GM_deleteValue(name)); location.reload(); }); const toggleIframe = () => { DOC.body.parentNode.classList.toggle("x-scrollbar-hide"); iframe.classList.toggle("x-show"); }; const openModal = () => { if (iframe.classList.contains("x-show")) return; toggleIframe(); _DOC.querySelector("#openControlPanel").click(); }; GM_registerMenuCommand("控制面板", openModal, "s"); _DOC.querySelector("#controlPanel").addEventListener("hidden.bs.modal", toggleIframe); }, { once: true } ); } // G_DARK globalDark = (style = "", dmStyle = "") => { if (!style && !dmStyle) return; const { common, dark } = this.style; if (style && common) style = `${common}${style}`; if (!this.G_DARK) dmStyle = ""; if (dmStyle && dark) dmStyle = `${dark}${dmStyle}`; const css = `${style}${dmStyle}`; if (css) GM_addStyle(css); }; // G_SEARCH globalSearch = () => { if (!this.G_SEARCH) return; const { selectors, pathname } = this.search; if (selectors) { DOC.addEventListener("keyup", ({ code }) => { if (code !== "Slash" || ["INPUT", "TEXTAREA"].includes(DOC.activeElement.nodeName)) return; DOC.querySelector(selectors)?.focus(); }); } if (pathname) { DOC.addEventListener("keydown", async e => { if (e.ctrlKey && e.code === "Slash") { const text = await navigator.clipboard.readText(); if (text) openInTab(`${location.origin}${pathname}#jump#${REP}`.replaceAll(REP, text)); } }); } }; // G_CLICK globalClick = () => { const { selectors, codeQuery, upQuery } = this.click; if (!selectors?.length) return; selectors.push(".x-jump"); const getTarget = ({ target }) => target.closest(selectors) || false; DOC.addEventListener("click", e => { const closest = getTarget(e); if (!closest) return; let url = ""; let code = ""; const { classList, dataset } = e.target; if (classList.contains("x-player") && dataset.pc) { return this.advancedLink(e, { type: "left" }); } else if (classList.contains("x-btn") && dataset.code) { return this.driveListOffLine(e, dataset); } else if (classList.contains("x-jump") && dataset.href) { url = dataset.href; } else if (closest.href) { url = closest.href; if (codeQuery) code = closest.querySelector(codeQuery)?.textContent; } if (!url) return; e.preventDefault(); e.stopPropagation(); if (this.G_CLICK) { const tab = openInTab(url); if (code) tab.onclose = () => this.driveMatchForList([{ item: closest, code }], upQuery); return; } location.href = url; }); if (!this.G_CLICK) return; let _event; DOC.addEventListener("mousedown", e => { if (e.button !== 2) return; const target = getTarget(e); if (!target) return; e.preventDefault(); target.oncontextmenu = e => e.preventDefault(); _event = e; }); DOC.addEventListener("mouseup", e => { if (e.button !== 2 || !_event) return; const target = getTarget(e); if (!target) return; e.preventDefault(); const { clientX, clientY } = e; const { clientX: _clientX, clientY: _clientY } = _event; if (Math.abs(clientX - _clientX) + Math.abs(clientY - _clientY) > 5) return; if (target.dataset.href) return openInTab(target.dataset.href, false); const tab = openInTab(target.href, false); const code = codeQuery ? target.querySelector(codeQuery)?.textContent : ""; if (code) tab.onclose = () => this.driveMatchForList([{ item: target, code }], upQuery); }); }; // L_MIT listMovieImgType = (node, condition) => { if (!this.L_MIT) return; const img = node.querySelector("img"); if (!img) return; node.classList.add("x-cover"); const { src = "" } = img; img.src = condition.find(({ regex }) => regex.test(src))?.replace(src) ?? src; }; // L_MV listMovieVideo = container => { if (!container.querySelector(".movie-list:is(.h, .cols-3) .cover, .movie-box.x-cover")) return; const DELAY = parseInt(this.L_MV ?? 0, 10); if (DELAY <= 0) return; let locking = null; unsafeWindow.addEventListener("scroll", () => { if (locking) clearTimeout(locking); if (!container.style.pointerEvents) container.style.pointerEvents = "none"; locking = setTimeout(() => { container.style.pointerEvents = ""; }, 100); }); const { selectors, codeQuery, upQuery } = this.click; const ITEM_NODE = selectors[0]; const LOCK_NODE = "x-loading"; const HOLD_NODE = "x-holding"; let current = null; let timer = null; DOC.addEventListener("mouseover", ({ relatedTarget, target }) => { if (current) { if (timer && relatedTarget?.nodeName === "IMG" && target.contains(relatedTarget)) { clearTimeout(timer); current = null; } return; } target = target.closest(upQuery); if (!target) return; if (!container.contains(target)) return; current = target; showPreview(current); }); DOC.addEventListener("mouseout", ({ relatedTarget }) => { if (!current) return; while (relatedTarget) { if (relatedTarget === current) return; relatedTarget = relatedTarget.parentNode; } hidePreview(current); current = null; }); const showPreview = cover => { cover.classList.add("x-preview", HOLD_NODE); timer = setTimeout(() => getVideo(cover), DELAY < 100 ? 100 : DELAY); }; const hidePreview = cover => { cover.classList.remove(HOLD_NODE); if (timer) clearTimeout(timer); const video = cover.querySelector("video"); if (!video) return; video.classList.remove("x-in"); setTimeout(() => video.remove(), 250); }; const getVideo = cover => { const closest = cover.closest(ITEM_NODE); if (!closest || closest.classList.contains(LOCK_NODE)) return; let code = closest.querySelector(codeQuery)?.textContent.trim() ?? ""; if (!code) return; code = /^FC2-\d/i.test(code) ? code.replace("-", "-PPV-") : code; let { video, poster } = cover.dataset; if (!video) { const detail = Store.getDetail(code); if (detail.video) { video = detail.video; cover.setAttribute("data-video", video); } } if (!poster) { poster = cover.querySelector("img")?.src ?? ""; cover.setAttribute("data-poster", poster); } if (video) return setVideo(cover); closest.classList.add(LOCK_NODE); const _timer = setTimeout(() => cancelLock("timeout"), 8000); let count = FC2_REGEX.test(code) ? 7 : 6; const cancelLock = status => { if (_timer) clearTimeout(_timer); count = 0; if (status === "success" && cover.classList.contains(HOLD_NODE)) { return closest.classList.remove(LOCK_NODE); } closest.classList.add(status); setTimeout(() => closest.classList.remove(status, LOCK_NODE), 800); }; const callback = ({ video }) => { if (count <= 0) return; if (!video) { count--; if (count <= 0) cancelLock("fail"); } else { cancelLock("success"); cover.setAttribute("data-video", video); Store.upDetail(code, { video }); setVideo(cover); } }; if (count === 7) Apis.fetchFC2(code).then(callback); Apis.fetchAVPreview(code).then(callback); Apis.fetchJavSpyl(code).then(callback); Apis.fetchVBGFL(code).then(callback); Apis.fetchDMM({ code }).then(({ fetchVideo }) => { if (fetchVideo) fetchVideo().then(callback); }); const isVR = VR_REGEX.test(closest.textContent); Apis.fetchVideoByGuess({ code, isVR }).then(callback); Apis.fetchVBL({ node: closest, isVR }).then(callback); }; const setVideo = cover => { if (!cover.classList.contains(HOLD_NODE) || cover.querySelector("video")) return; let { video, poster } = cover.dataset; video = DOC.create("video", { poster, src: video, "x-webkit-airplay": "deny", controlslist: "nodownload nofullscreen noremoteplayback noplaybackrate", preload: "metadata", title: "", }); video.autoplay = true; video.autoPictureInPicture = false; video.controls = true; video.currentTime = 3; video.disablePictureInPicture = true; video.disableRemotePlayback = true; video.loop = true; video.muted = true; video.playsInline = true; video.volume = localStorage.getItem("volume") ?? 0.2; video.addEventListener("keyup", ({ code, target }) => { if (code === "KeyM") target.muted = !target.muted; }); cover.append(video); video?.focus(); video?.load(); setTimeout(() => video?.classList?.add("x-in"), 50); }; }; // L_MTH, L_MTL listMovieTitle = () => { let num = parseInt(this.L_MTL ?? 0, 10); if (this.L_MTH && num < 1) num = 1; return `.x-ellipsis { -webkit-line-clamp: ${num <= 0 ? "unset" : num}; ${this.L_MTH ? `height: calc(var(--x-line-h) * ${num}) !important;` : ""} }`; }; // L_SCROLL listScroll = async function ({ container, itemSelector = ".item", path, start, showPage, onLoad }) { let { list: domList = [DOC], active: isMerge = false } = this.merge; const getItems = async list => { list = await Promise.all(list.map(item => (typeof item !== "string" ? item : request(item)))); list = list.filter(Boolean); if (!list.length) return list; domList = list; list = list.map(item => [...item.querySelectorAll(itemSelector)]).flat(); if (!list.length) return list; return this.modifyItem(list, isMerge); }; const items = await getItems(domList); const { length } = items; start(items); if (typeof container === "string") container = DOC.querySelector(container); container.classList.add("x-in"); if (length) this.listMovieVideo(container); if (!isMerge && !this.L_SCROLL) return showPage(); const hasMore = "加载中..."; const noMore = "没有更多了"; const status = DOC.create("div", { id: "x-status" }, length ? hasMore : noMore); container.insertAdjacentElement("afterend", status); if (!length) return; let urls = []; const getNext = list => { urls = list.map(item => item.querySelector(path)?.href).filter(Boolean); addPrefetch(urls); }; getNext(domList); if (!urls.length) { status.textContent = noMore; return; } let isLoading = false; let observer = new IntersectionObserver(async entries => { if (!urls.length) { status.textContent = noMore; return observer.disconnect(); } if (!entries[0].isIntersecting || isLoading) return; status.textContent = hasMore; isLoading = true; const _items = await getItems(urls); isLoading = false; if (!_items.length) { status.textContent = "请求失败,检查后滚动以重试"; return; } getNext(domList); onLoad(_items); }); observer.observe(status); }; // L_MERGE listMerge = () => { let merge = this.L_MERGE.trim(); if (!merge) return; merge = merge.split("\n\n").filter(Boolean); if (!merge.length) return; const list = {}; const regex = /^\[.+\]$/; const replace = /\[|\]/g; for (const item of merge) { const res = unique(item.split("\n").filter(Boolean)); if (res.length <= 1) continue; const first = res.shift(); if (!regex.test(first)) continue; list[first.replace(replace, "")] = res; } const keys = Object.keys(list); if (!keys.length) return; let active = ""; const { activePath = "/", start } = this.merge; if (location.pathname === activePath) { const { search } = location; if (search.startsWith("?merge=")) { active = decodeURIComponent(search.split("=").at(-1)); if (!keys.includes(active)) active = ""; } } DOC.addEventListener("XContentLoaded", () => start(keys, active), { once: true }); if (!active) return; this.merge.active = active; this.merge.list = list[active].map(item => (item.split("#")[0] === activePath ? DOC : item)); DOC.title = `${active} - 合并列表 - ${Domain}`; }; // L_FILTER listFilter = ({ code = "", title = "", score, tags }) => { const filter = this.L_FILTER.trim(); if (!filter || (!code && !title)) return; const hlRegex = /^\^/; tags = tags?.length ? Array.from(tags) .map(item => item.textContent) .join(",") : ""; const match = (rule, word) => { if (rule.endsWith("#start")) { return word.startsWith(rule.replace(/#start$/, "")); } else if (rule.endsWith("#end")) { return word.endsWith(rule.replace(/#end$/, "")); } else { if (!rule.startsWith("reg:")) return word.includes(rule); rule = rule.replace(/^reg:/, ""); return rule ? eval(rule)?.test(word) : rule; } }; let res = ""; for (const item of filter.split("\n\n")) { let word = ""; if (item.startsWith("[code]")) word = code; if (item.startsWith("[title]")) word = title; if (item.startsWith("[score]")) word = score; if (item.startsWith("[tags]")) word = tags; if (!word && word !== "") continue; const rules = unique(item.split("\n").filter(Boolean)); if (rules.length <= 1) continue; const highlight = []; for (const rule of rules.slice(1)) { if (hlRegex.test(rule)) { highlight.push(rule.replace(hlRegex, "")); continue; } if (match(rule, word)) res = "x-hide"; if (res === "x-hide") break; } if (res === "x-hide") break; for (const rule of highlight) { if (match(rule, word)) res = "x-highlight"; if (res === "x-highlight") break; } } return res; }; // M_RES, M_TITLE, M_STAR, M_SCORE, M_SUB, M_MAGNET getMovieResource = function ({ video, transTitle, star, code, cid, studio, isVR, title }) { let res = []; if (this.M_RES?.trim()) { res = paramParse(this.M_RES, ["img", "video", "player"]); this.params.res = res; } if (this.M_SCORE?.trim()) { const param = paramParse(this.M_SCORE, ["db", "lib", "dmm", "mgs"]); const _res = param.filter(item => Domain !== "JavDB" || item !== "db").map(item => `${item}_score`); res = [...res, ..._res]; if (_res.length) this.params.score = param; } if (this.M_MAGNET?.trim()) { const param = paramParse(this.M_MAGNET, ["suk", "bts", "btd"]); res = [...res, ...param.map(item => `${item}_magnet`)]; this.params.magnet = param; } if (this.M_TITLE) res.push("title"); if (!star?.length && this.M_STAR) res.push("star"); if (this.M_SUB) res.push("sub"); const needs = []; const detail = Store.getDetail(code); const resource = { video, title: transTitle }; for (const key of res) { let val = detail[key]; this.params[key] = val; if (val?.length) continue; val = resource[key]; if (val?.length) { this.setMovieResource({ [key]: val }); continue; } needs.push(key); } if (!needs.length) return; if (needs.includes("img")) { DOC.head.insertAdjacentHTML("beforeend", '<meta name="referrer" content="never">'); let _code = code; if (/^HEYZO-\d/i.test(_code)) _code = _code.replace("-", " "); Apis.fetchJavStore(_code).then(res => res.img && this.setMovieResource(res)); Apis.fetchBlogJav(_code).then(res => this.setMovieResource(res)); } if (needs.includes("video")) { Apis.fetchVideoByGuess({ code, isVR, cid }).then(res => res.video && this.setMovieResource(res)); Apis.fetchVideoByStudio({ code, studio }).then(res => res.video && this.setMovieResource(res)); Apis.fetchJavSpyl(code).then(res => this.setMovieResource(res)); Apis.fetchAVPreview(code).then(res => this.setMovieResource(res)); if (FC2_REGEX.test(code)) Apis.fetchFC2(code).then(res => this.setMovieResource(res)); } if (needs.includes("player")) Apis.fetchNetflav(code).then(res => this.setMovieResource(res)); if (needs.includes("title")) Apis.fetchTranslate(title).then(res => this.setMovieResource(res)); if (needs.includes("suk_magnet")) Apis.fetchSukebei(code).then(res => this.setMovieResource(res)); if (needs.includes("bts_magnet")) Apis.fetchBTSOW(code).then(res => this.setMovieResource(res)); if (needs.includes("btd_magnet")) { Apis.fetchBTDigg(code).then(res => { if (res.btd_magnet.length) return this.setMovieResource(res); Apis.fetchBTDigg(code, 1).then(re => this.setMovieResource(re)); }); } const isMGS = ["mgs_score", "video"]; if (needs.some(item => isMGS.includes(item))) { Apis.fetchMGS({ code, title }).then(res => this.setMovieResource(res)); } const isLib = ["lib_score", "star"]; if (needs.some(item => isLib.includes(item))) Apis.fetchLib(code).then(res => this.setMovieResource(res)); const isDMM = ["dmm_score", "star", "video"]; if (needs.some(item => isDMM.includes(item))) { Apis.fetchDMM({ code, title }).then(({ fetchVideo, fetchInfo }) => { if (!fetchVideo) return this.setMovieResource({ dmm_score: [], star: [], video: "" }); if (needs.includes("video")) fetchVideo().then(re => this.setMovieResource(re)); if (needs.includes("dmm_score")) fetchInfo().then(re => this.setMovieResource(re)); }); } if (Domain === "JavDB") return; const isDB = ["db_score", "star", "video", "sub"]; if (needs.some(item => isDB.includes(item))) Apis.fetchDB(code).then(res => this.setMovieResource(res)); }; setMovieResource = function (res) { if (!res) return; const { code } = this.info; const detail = Store.getDetail(code); for (const [key, val] of Object.entries(res)) { if (detail[key]?.length) continue; if (val?.length) Store.upDetail(code, { [key]: val }); if (this.params.hasOwnProperty(key)) this.params[key] = val; } }; upMovieResource = function (key, update, start) { if (!this.params.hasOwnProperty(key)) return; start?.(); if (this.params[key]?.length) return update(this.params[key]); Object.defineProperty(this.params, key, { set: update, get: undefined }); }; // M_JUMP movieJump = ({ code }, start, update) => { if (!code || !start || !update) return; let jump = this.M_JUMP?.trim().split("\n\n").filter(Boolean); let { length } = jump; if (!length) return; const regex = /^\[.+\]$/; const replace = /\[|\]/g; const urlReg = /^https?:\/\/[a-z0-9]+/i; const group = []; for (let index = 0; index < length; index++) { const items = unique(jump[index].split("\n").filter(Boolean)); if (items.length <= 1 || !regex.test(items[0])) continue; const list = items.filter(item => urlReg.test(item)); if (!list.length) continue; group.push({ group: items[0].replace(replace, ""), list: list.map(item => { return { url: `${item.replaceAll(REP, code).trim()}#${code}`, query: item.includes("#query"), name: item.split("//")[1].split("/")[0].split(".").at(-2), }; }), }); } if (group.length) start(group); const nodeList = DOC.querySelectorAll(".x-jump"); length = nodeList.length; if (!length) return; jump = Store.getDetail(code)?.jump ?? []; for (let index = 0; index < length; index++) { const node = nodeList[index]; const { query, href } = node.dataset; if (query !== "true") continue; const key = href.split("#")[0]; let item = jump.find(item => item.key.toLowerCase() === key.toLowerCase()); if (item) { if (item.href) update(item, node); continue; } request(key).then(dom => { if (!dom) return; item = captureQuery(code, dom); if (!item?.href) return; update(item, node); jump.push({ ...item, key }); Store.upDetail(code, { jump }); }); } }; // M_SORT movieSort = (magnets, start) => { if (!this.M_SORT) return magnets; start?.(); return magnets.length <= 1 ? magnets : magnets.sort((pre, next) => { if (pre.zh === next.zh) { if (pre.bytes === next.bytes) return next.date - pre.date; return next.bytes - pre.bytes; } else { return pre.zh > next.zh ? -1 : 1; } }); }; // D_MATCH driveMatchForList = async (items, selectors) => { if (!this.D_MATCH) return; const update = ({ item }, res) => { const node = item.querySelector(selectors); if (!node) return; const keys = ["n", "pc", "fid", "cid"]; const { classList, dataset } = node; if (!res.length) { keys.forEach(key => delete dataset[key]); node.removeAttribute("title"); return classList.remove("x-zh", "x-player"); } classList.add("x-player"); let str = "已有"; let _item = res[0]; const zh = res.find(({ n }) => ZH_REGEX.test(n)); if (zh) { classList.add("x-zh"); str = "字幕"; _item = zh; } node.setAttribute("title", `${str}资源`); keys.forEach(key => { dataset[key] = _item[key]; }); }; const params = {}; for (let index = 0, { length } = items; index < length; index++) { const item = items[index]; let { code } = item; code = /^FC2-\d/i.test(code) ? code.replace("-", "-PPV-") : code; item.code = code; const { res } = Store.getDetail(code); if (res) { update(item, res); continue; } const key = code.split("-")[0]; params.hasOwnProperty(key) ? params[key].push(item) : (params[key] = [item]); } for (const [prefix, items] of Object.entries(params)) { let res = Store.getResource(prefix); if (!res) { res = await Apis._driveSearch(prefix); if (!res) break; Store.upResource(prefix, res); } if (!res.length) continue; for (let index = 0, { length } = items; index < length; index++) { const item = items[index]; const { regex } = codeParse(item.code); const _res = res.filter(({ n }) => regex.test(n)); if (_res.length) update(item, _res); } } }; driveMatchForMovie = async function ({ code }, update, start) { if (!this.D_MATCH) return; start?.(); const cid = await this.driveCid(); if (cid === undefined) return update([]); const { prefix, regex } = codeParse(code); const list = await Promise.allSettled([Apis._driveSearch(prefix, ["t"]), Apis.driveVideo(cid)]); let res; for (let index = 0, { length } = list; index < length; index++) { const { status, value } = list[index]; if (status !== "fulfilled" || !value?.length) continue; if (!res) res = []; for (let idx = 0, len = value.length; idx < len; idx++) { const item = value[idx]; if (res.some(re => re.fid === item.fid) || !regex.test(item.n)) continue; res.push(item); } } update(res || []); if (res) Store.upDetail(code, { res }); }; // D_LOL upLOLTarget = () => { if (!this.D_MATCH || !this.D_LOL) return; GM_addStyle('.x-btn{display:revert}.x-btn::after{content:"离线"}'); }; driveListOffLine = async (e, { code, title }) => { e.preventDefault(); e.stopPropagation(); const LOCK = "active"; const { classList } = e.target; if (classList.contains(LOCK)) return; classList.add(LOCK); const magnet = paramParse(this.M_MAGNET, ["suk", "bts", "btd"]); if (!magnet.length) return classList.remove(LOCK); const detail = Store.getDetail(code); let magnets = magnet.map(item => detail[`${item}_magnet`] ?? []).flat(); if (!magnets.length) { const list = { suk: () => Apis.fetchSukebei(code), bts: () => Apis.fetchBTSOW(code), btd: () => Apis.fetchBTDigg(code), }; const res = await Promise.allSettled(magnet.map(item => list[item]())); for (let index = 0, { length } = res; index < length; index++) { const { status, value } = res[index]; if (status !== "fulfilled" || !value) continue; Store.upDetail(code, value); magnets.push(...value[`${magnet[index]}_magnet`]); } } if (!magnets.length) return classList.remove(LOCK); magnets = this.movieSort(magnets); magnets = this.driveFail(magnets); if (/^\$\{.+\}$/.test(this.D_CID)) this.D_CID = ""; const [wp_path_id, sign] = await Promise.all([this.driveCid("create"), Apis.driveSign()]); if (!wp_path_id || !sign) return classList.remove(LOCK); for (let index = 0, { length } = magnets; index < length; index++) { const isLast = index + 1 === length; const { link: url, zh } = magnets[index]; let res = await Apis.driveAddTask({ url, wp_path_id, ...sign }); if (!res) break; const { state, errcode, error_msg } = res; if (!state) { if ([10008, 10007].includes(errcode)) { if (errcode === 10008 && !isLast) continue; notify({ type: "warn", title: error_msg }); break; } if (errcode === 911) { classList.add("pending"); if (Store.getVerifyStatus() !== "pending") { notify({ type: "warn", title: `${code} 任务暂停,等待校验` }); verify(); } const listener = GM_addValueChangeListener( "VERIFY_STATUS", (name, old_value, new_value, remote) => { if (!remote || !["verified", "failed"].includes(new_value)) return; GM_removeValueChangeListener(listener); classList.remove(LOCK, "pending"); if (new_value === "verified") this.driveListOffLine(e, { code, title }); } ); break; } } const cid = wp_path_id; const clickUrl = FILE_RUL.replace(REP, cid); res = await this.driveVerify({ code, cid }); if (!res) { if (!isLast) continue; notify({ type: "info", title: `${code} 验证失败`, clickUrl }); break; } if (res?.length) { notify({ type: "success", title: `${code} 离线成功`, text: `点击跳转目录`, clickUrl: FILE_RUL.replace(REP, res[0].cid), }); Store.upDetail(code, { res }); const { selectors, upQuery } = this.click; const item = e.target.closest(selectors); if (item) this.driveMatchForList([{ item, code }], upQuery); const list = [() => this.driveClear({ cid, res, code })]; const { fetch } = this.driveRename({ cid, res, zh, code, title }); if (fetch) list.push(() => fetch()); await Promise.allSettled(list.map(item => item())); } else { notify({ type: "info", title: `${code} 任务结束`, clickUrl }); } break; } if (!classList.contains("pending")) classList.remove(LOCK); }; // D_CID driveCid = async function (type = "find") { let cid = this.D_CID.trim() || "${云下载}"; if (/^\$\{.+\}$/.test(cid)) { let isFirst = true; const arr = cid .replace(/\$|\{|\}/g, "") .split("/") .map(item => item.trim()) .filter(Boolean); for (let index = 0, { length } = arr; index < length; index++) { let item = arr[index]; if (!item.startsWith("#")) { cid = item; break; } item = item.replace(/^#/, ""); if (!item) continue; cid = this.info?.[item]; if (!cid?.length) { isFirst = false; continue; } if (typeof cid !== "string") cid = cid[0]; break; } if (!cid?.length) cid = "云下载"; cid = await Apis._driveCid(cid, type); if (cid && (isFirst || type !== "find")) this.D_CID = cid; } return cid; }; // D_VERIFY driveVerify = async ({ code, cid }) => { let verify = this.D_VERIFY <= 0; const { regex } = codeParse(code); const exist = Store.getDetail(code)?.res ?? []; for (let idx = 0; idx < this.D_VERIFY; idx++) { await delay(); let res = await Apis.driveVideo(cid, ["ico"]); res = res.filter(({ n, t }) => regex.test(n) && t.startsWith(getDate())); if (!res?.length) continue; res = res.filter(item => !exist.find(e => e.fid === item.fid)); if (!res.length) continue; verify = res; break; } return verify; }; // D_FAIL driveFail = magnets => { const num = parseInt(this.D_FAIL ?? 0, 10); return num <= 0 ? magnets : magnets.slice(0, num); }; // D_CLEAR driveClear = ({ cid, res, code }) => { if (!this.D_CLEAR) return; const { regex } = codeParse(code); unique(res.map(item => item.cid).filter(item => item !== cid)).forEach(async pid => { let fids = await Apis.driveFile(pid); if (!fids?.data) return; fids = fids.data.filter(({ n }) => !regex.test(n)); if (fids.length) Apis.driveClear({ pid, fids }); }); }; // D_RENAME driveRename = ({ cid, res, zh, code, title }) => { let file_name = this.D_RENAME?.trim(); if (!file_name) return { file_name: `${code} ${title}` }; file_name = file_name .replace(/\$\{字幕\}/g, zh ? "【中文字幕】" : "") .replace(/\$\{番号\}/g, code) .replace(/\$\{标题\}/g, title); if (!codeParse(code).regex.test(file_name)) file_name = `${code} - ${file_name}`; res = res.filter(item => item.ico); const noRegex = /\$\{序号\}/g; const data = []; unique(res.map(item => `${item.cid}/${item.ico}`)).forEach(key => { const [_cid, _ico] = key.split("/"); res.filter(item => item.cid === _cid && item.ico === _ico).forEach((item, index) => { data.push({ ...item, file_name: `${file_name.replace(noRegex, index + 1)}.${_ico}` }); }); }); file_name = file_name.replace(noRegex, ""); unique(res.map(item => item.cid).filter(item => item !== cid)).forEach(fid => { data.push({ fid, file_name }); }); return { file_name, fetch: () => Apis.driveRename(data) }; }; // D_TAG driveTag = async function (file_ids) { if (!file_ids?.length || !this.D_TAG?.trim()) return; let tag = paramParse(this.D_TAG, ["genre", "star"]); if (!tag.length) return; tag = tag.map(key => this.info[key] ?? []); tag = tag .flat() .map(item => item.trim()) .filter(Boolean); if (!tag.length) return; const label = await Apis.driveLabel(); if (!label?.length) return; let file_label = unique(tag).map(item => label.find(({ name }) => name === item)?.id ?? ""); file_label = file_label.filter(Boolean); if (file_label.length) Apis.driveTag({ file_ids, file_label: file_label.join(",") }); }; // D_UPLOAD driveUpload = async function ({ cid, name, res }) { const keys = res; res = res.map(key => this.info[key]).filter(Boolean); const list = await Promise.allSettled(res.map(item => request(item, {}, "GET", { responseType: "blob" }))); const target = `U_1_${cid}`; for (let index = 0, { length } = list; index < length; index++) { const { status, value } = list[index]; if (status === "rejected" || !value?.size) continue; const filename = `${name}_${keys[index]}_.${res[index].split(".").at(-1)}`; const init = await Apis.driveInitUpload({ filename, filesize: value.size, target }); if (!init?.host) continue; await Apis.driveUpload(init, value, filename); } notify({ text: "", type: "", title: "图传任务结束" }); }; // D_MODIFY upModifyTarget = () => { const modify = paramParse(this.D_MODIFY, ["clear", "rename", "tag", "upload", "delete"]); if (!modify.length) return modify; GM_addStyle(".x-btn{display:revert}"); if (!modify.includes("delete")) return modify; GM_addStyle(".x-btn{background:var(--x-red)}.x-btn::after{content:'删除'}"); return modify; }; driveModify = async function (e, modify) { e.preventDefault(); e.stopPropagation(); const { classList, parentNode } = e.target; if (classList.contains("active")) return; classList.add("active"); const { cid, fid, n } = parentNode.dataset; const list = []; if (modify.includes("delete")) { list.push(() => Apis.driveClear({ pid: cid, fids: [{ fid }] })); } else { const { code, title } = this.info; const { regex } = codeParse(code); let file_name = this.D_RENAME?.trim(); if (file_name) { file_name = file_name .replace(/\$\{字幕\}/g, ZH_REGEX.test(n) ? "【中文字幕】" : "") .replace(/\$\{番号\}/g, code) .replace(/\$\{标题\}/g, title) .replace(/\$\{序号\}/g, ""); if (!regex.test(file_name)) file_name = `${code} - ${file_name}`; } if (modify.includes("rename") && file_name) { let name = n.split("."); name.pop(); if (name.join("") !== file_name) list.push(() => Apis.driveRename([{ fid, file_name }])); } if (modify.includes("tag")) list.push(() => this.driveTag(fid)); if (modify.some(item => ["clear", "upload"].includes(item))) { const { data } = await Apis.driveFile(cid); if (modify.includes("clear")) { const fids = data.filter(({ n }) => !regex.test(n)); if (fids.length) list.push(() => Apis.driveClear({ pid: cid, fids })); } if (modify.includes("upload")) { let uploads = paramParse(this.D_UPLOAD, ["cover", "img"]); if (uploads.length) { const items = data.filter(item => regex.test(item.n) && item.class === "PIC"); uploads = uploads.filter(item => !items.some(({ n }) => n.includes(`_${item}_`))); if (uploads.length) { file_name = file_name || `${code} ${title}`; list.push(() => this.driveUpload({ cid, name: file_name, res: uploads })); } } } } } if (list.length) await Promise.allSettled(list.map(item => item())); classList.remove("active"); if (!DOC.querySelector(".x-btn.active")) this.driveMatch(); }; // OFFLINE driveOffLine = async function (e, { code, title, magnets }) { const { target } = e; const { magnet: type } = target.dataset; if (!type) return; e.preventDefault(); e.stopPropagation(); const setTab = type === "all"; magnets = setTab ? this.driveFail(magnets) : magnets.filter(item => item.link === type); const length = magnets?.length ?? 0; if (!length) return; const { classList } = target; classList.remove("pending"); classList.add("active"); const originText = setTab ? "一键离线" : "添加离线"; target.textContent = "请求中..."; target.inert = true; const handleComplete = () => { classList.remove("active"); target.textContent = originText; target.inert = false; }; const [wp_path_id, sign] = await Promise.all([this.driveCid("create"), Apis.driveSign()]); if (!wp_path_id || !sign) return handleComplete(); const warnTitle = `${code} 一键离线失败`; if (setTab) setTabBar({ title: `${code} 一键离线中...` }); for (let index = 0; index < length; index++) { const isLast = index + 1 === length; const { link: url, zh } = magnets[index]; let res = await Apis.driveAddTask({ url, wp_path_id, ...sign }); if (!res) { if (!index) return handleComplete(); break; } const { state, errcode, error_msg } = res; if (!state) { if ([10008, 10007].includes(errcode)) { if (errcode === 10008 && !isLast) continue; notify({ type: "warn", title: error_msg }); if (setTab) setTabBar({ type: "warn", title: warnTitle }); break; } if (errcode === 911) { classList.add("pending"); target.textContent = "校验中..."; const msg = { type: "warn", title: `${code} 任务暂停,等待校验` }; if (setTab) setTabBar(msg); if (Store.getVerifyStatus() !== "pending") { notify(msg); verify(); } const listener = GM_addValueChangeListener( "VERIFY_STATUS", (name, old_value, new_value, remote) => { if (!remote || !["verified", "failed"].includes(new_value)) return; GM_removeValueChangeListener(listener); if (new_value !== "verified") return handleComplete(); this.driveOffLine(e, { magnets: magnets.slice(index), code, title }); } ); break; } } const cid = wp_path_id; const clickUrl = FILE_RUL.replace(REP, cid); res = await this.driveVerify({ code, cid }); if (!res) { if (!isLast) continue; notify({ type: "info", title: `${code} 验证失败`, clickUrl, setTab }); break; } if (res?.length) { const _cid = res[0].cid; const uploads = paramParse(this.D_UPLOAD, ["cover", "img"]); notify({ type: "success", title: `${code} 离线成功`, setTab, text: `点击跳转目录${uploads.length ? ",尝试图传中..." : ""}`, clickUrl: FILE_RUL.replace(REP, _cid), }); const { file_name, fetch } = this.driveRename({ cid, res, zh, code, title }); if (fetch) await fetch(); if (uploads.length) this.driveUpload({ cid: _cid, name: file_name, res: uploads }); this.driveTag( res .map(({ fid }) => fid) .filter(Boolean) .join(",") ); this.driveClear({ cid, res, code }); } else { notify({ type: "info", title: `${code} 任务结束`, clickUrl, setTab }); } break; } if (classList.contains("pending")) return; await delay(); this.driveMatch(); handleComplete(); }; // A_LINK advancedLink = async (e, { type = "left", defaultLink }) => { e.preventDefault(); e.stopPropagation(); const { n: name, pc: pickcode, fid, cid } = e.target.dataset; defaultLink = defaultLink ?? type === "left" ? `${PICK_RUL}${pickcode}` : FILE_RUL.replace(REP, cid); if (!fid || fid === "undefined") return openInTab(defaultLink); let config = this.A_LINK?.trim()?.split("\n"); let index = config.findIndex(item => item === `[${type}]`); if (index === -1) return openInTab(defaultLink); config = config[index + 1]?.trim(); if (!config) return openInTab(defaultLink); config = config .replaceAll("${#pickcode}", pickcode) .replaceAll("${#cid}", cid) .replaceAll("${#name}", encodeURIComponent(name)); if (!config.includes("${#path}")) return openInTab(config); let path = await Apis.driveDetail(fid); if (!path?.paths?.length) return; path = path.paths .map(item => item.file_name) .splice(1) .join("/"); openInTab(config.replaceAll("${#path}", encodeURIComponent(path))); }; } class JavDB extends Common { constructor() { super(); return super.init(); } excludeMenu = ["G_DARK", "L_MIT", "M_SUB"]; routes = { list: /^\/($|guess|(un)?censored|western|fc2|anime|(advanced_)?search|video_codes|tags|rankings|actors|series|makers|directors|publishers|lists|users)/i, movie: /^\/v\/\w+/i, others: /.*/i, }; _style = { common: ` html { overflow: overlay; padding-bottom: 0 !important; } body { min-height: 100%; } .section { padding: 20px; } #search-bar-container { margin-bottom: 8px !important; } .search-panel button { border: none; } #search-type, #video-search { z-index: auto; border: none; box-shadow: none; } .title:not(:last-child) { margin-bottom: 20px; } .main-title { padding-top: 0; } .app-desktop-banner, #footer { display: none !important; } :root[data-theme="dark"] ::-webkit-scrollbar-thumb { background: var(--x-grey) !important; } a.box:is(:focus, :hover) { box-shadow: none !important; } :root[data-theme="dark"] .box:hover { background: unset; } :root[data-theme="dark"] img { filter: brightness(.9) contrast(.9) !important; } nav.pagination { display: none; margin: 0 -4px 20px !important; padding: 0; border: none; } :root[data-theme="dark"] nav.pagination { border: none !important; } .float-buttons { right: 8px; } `, }; search = { selectors: "#video-search", pathname: `/search?q=${REP}&f=all`, }; click = { selectors: [":is(.movie-list, .actors, .section-container) a:has(strong)"], codeQuery: ".video-title strong", upQuery: ".cover", }; merge = { start: list => { DOC.querySelector("#navbar-menu-hero .navbar-start")?.insertAdjacentHTML( "beforeend", `<div class="navbar-item has-dropdown is-hoverable"> <a class="navbar-link" href="/?merge=${list[0]}">合并列表</a> <div class="navbar-dropdown is-boxed"> ${list.map(item => `<a class="navbar-item" href="/?merge=${item}">${item}</a>`).join("")} </div> </div>` ); }, }; list = { docStart() { const style = ` .awards, .form-panel .user-profile, .section:has(.actors, .movie-list, .section-container, .user-container .common-list) { padding-bottom: 0; } :is(.tabs, .message):not(:last-child) { margin-bottom: 20px; } .movie-list { opacity: 0; } .actors, .movie-list, .section-container { gap: 20px !important; margin: 0 0 20px !important; padding-bottom: 0; } .container > br, .actor-filter hr, .movie-list:has(.tags), .user-container > .column.is-10 > .message.is-warning { display: none; } .main-tabs, .box:has(> form) { margin-bottom: 20px !important; } .toolbar { margin-top: -10px; padding-bottom: 0; word-spacing: -20px; } .toolbar * { word-spacing: 0; } .toolbar .button-group, .actor-tags .content .tag { margin: 0 10px 10px 0; } #t { height: 2.2rem; } #tags { margin: -20px 0 20px; } #tags dt a.tag-expand { margin-top: .44rem; } .section:has(> .awards, > .user-container) { padding: 0; } .divider-title { margin-bottom: 16px !important; padding-bottom: 0; border-bottom: none; } .actors .box { margin-bottom: 0; } .actor-filter-toolbar { padding-bottom: 20px; } .section-columns { margin-bottom: 8px !important; padding-top: 0; } .columns:has(> .section-addition) { margin-bottom: 8px; } .actor-tags { margin-bottom: 20px !important; padding-bottom: 0; font-size: 0; border-bottom: none; } .actor-tags .content:not(.collapse) { margin-bottom: -10px; } .user-container { margin: -10px -10px 0 !important; } .user-container > .column { padding: 10px 10px 0; } .user-container > .column:first-child { padding-bottom: 10px; } .user-container .common-list ul { margin-bottom: 20px; } .list-item:not(:last-child) { border-bottom: 1px solid #dbdbdb; } .user-container .common-list .list-item { align-items: center; margin: 0; padding: 10px 2px; } .user-container .common-list .list-item:first-child { padding-top: 0; } .user-container .common-list .list-item:last-child { padding-bottom: 0; } .user-container .common-list .list-item .column { padding: 0; } .user-container .common-list .list-item .column:first-child { flex: 1; } .user-container .common-list .list-item .column:last-child { width: auto; } `; const layout = ` @media (width < 576px) { .movie-list.v, .actors { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } .movie-list.h { grid-template-columns: repeat(1, minmax(0, 1fr)) !important; } } @media (width >= 576px) { .movie-list.v, .actors { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } .movie-list.h { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } } @media (width >= 768px) { .movie-list.v, .actors { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } .movie-list.h { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } } @media (width >= 992px) { .movie-list.v, .actors { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; } .movie-list.h { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } } @media (width >= 1200px) { .movie-list.v, .actors { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; } .movie-list.h { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } } @media (width >= 1400px) { .movie-list.v, .actors { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; } .movie-list.h { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } } .user-container .column:has(.movie-list, .actors) { container-type: inline-size; } @container (width < 576px) { .column .movie-list { grid-template-columns: repeat(1, minmax(0, 1fr)) !important; } .column .movie-list.v, .actors { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } } @container (width >= 576px) { .column .movie-list { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } .column .movie-list.v, .actors { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } } @container (width >= 768px) { .column .movie-list { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } .column .movie-list.v, .actors { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } } @container (width >= 992px) { .column .movie-list { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } .column .movie-list.v, .actors { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; } } @container (width >= 1200px) { .column .movie-list { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } .column .movie-list.v, .actors { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; } } @container (width >= 1400px) { .column .movie-list { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } .column .movie-list.v, .actors { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; } } `; const card = ` :is(.movie-list, .actors) .box { padding: 0 0 10px; } .movie-list .item .cover, .actor-box a figure { background: var(--x-sub-ftc); } :root[data-theme="dark"] :is(.movie-list .item .cover, .actor-box a figure) { background: var(--x-grey); } .movie-list .item .cover { padding: 0 !important; aspect-ratio: var(--x-cover-ratio); } .actors .box img { height: 100%; object-fit: cover; opacity: 0; } .movie-list .item .cover img { width: 100%; object-fit: contain; opacity: 0; } .movie-list .item .cover:hover img { z-index: auto; transform: none; } .movie-list.v .item .cover { aspect-ratio: var(--x-thumb-ratio); } .movie-list.v .item .cover img { object-fit: cover !important; } .movie-list .item .video-title, .actor-box a strong, .section-container .box { font-size: 14px; line-height: var(--x-line-h); } .movie-list .item .video-title { box-sizing: content-box; padding: 10px 0 0; } .movie-list .item .score { padding: 0; } .movie-list .item .box :is(.video-title, .score, .tags, .meta-buttons), .actor-box a strong { padding: 10px 10px 0; } .movie-list .item .meta { padding: 0 10px; } .movie-list .box .tags { min-height: 44px; margin-bottom: -10px; } .movie-list .box .tags .tag { margin-bottom: 10px; } .actor-box a figure { aspect-ratio: var(--x-avatar-ratio); } .actor-box a.button.is-danger { width: auto; margin: 10px 10px 0 !important; } .movie-list .box .meta-buttons a.button.is-danger { margin: 0 !important; } .section-container a:has(> strong) { display: flex; } .section-container .box strong { flex: 1; padding-right: 10px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } `; this.globalDark(`${this._style.common}${style}${layout}${card}${this.listMovieTitle()}`); this.listMerge(); this.upLOLTarget(); }, contentLoaded() { this.globalSearch(); if (location.pathname === "/users/favorite_lists") { this.click.selectors.push(".common-list .column.is-10 a"); } this.globalClick(); this.modifyLayout(); }, modifyLayout() { const tags = DOC.querySelector("#tags:not(select)"); if (tags) { const toggle = DOC.create("button", { class: "button is-info", type: "button" }, "折叠"); const tabs = DOC.querySelector(".tabs.is-boxed"); tabs.classList.add("is-align-items-flex-end"); tabs.append(toggle); toggle.addEventListener("click", () => tags.classList.toggle("is-hidden")); } for (const list of DOC.querySelectorAll(".movie-list:not(:has(.tags))")) { const _list = list.cloneNode(true); const items = this.modifyItem([..._list.querySelectorAll(".item")]); _list.innerHTML = ""; _list.append(...items); _list.classList.add("x-in"); list.replaceWith(_list); } let container; const lists = DOC.querySelectorAll(".actors"); const { length } = lists; if (length) { if (length === 1) { container = lists[0]; } else { for (let index = 0; index < length; index++) { for (const item of lists[index].querySelectorAll(".box")) fadeInImg(item); } } } container = container ?? DOC.querySelector([ ".movie-list:has(.tags)", ".section-container:has(.box)", ".user-container:has(nav.pagination) .common-list ul", ]); if (!container) { if (DOC.querySelector(".movie-list:is(.h, .cols-3) .cover")) this.listMovieVideo(DOC); return; } this.listScroll({ container, itemSelector: [ ".movie-list:has(.tags) .item", ".actors .box", ".section-container .box", ".common-list ul .list-item", ], path: ".pagination-next", start: items => { container.innerHTML = ""; container.append(...items); container.classList.add("x-grid"); }, showPage: () => DOC.querySelector("nav.pagination")?.classList.add("x-flex-center"), onLoad: items => container.append(...items), }); }, modifyItem(items, isMerge) { const isMovieList = items.some(item => item.querySelector(".video-title")); const res = []; const hlUrls = []; for (let index = 0, { length } = items; index < length; index++) { const item = items[index]; let code = ""; let date = ""; let score = 0; let highlight = false; if (isMovieList) { code = item.querySelector(".video-title strong"); if (!code) continue; code = code.textContent.trim(); date = item.querySelector(".meta")?.textContent ?? ""; date = date.replaceAll("-", "").trim(); if (isMerge) { if (res.some(t => t.code === code && t.date === date)) continue; score = (item.querySelector(".score")?.textContent ?? "").match(NUM_REGEX)[0] ?? 0; item.querySelector(".meta-buttons")?.remove(); } this.modifyMovieCard(item); if (item.matches(".x-hide")) continue; highlight = item.matches(".x-highlight"); if (highlight) hlUrls.push(item.querySelector("a").href); } fadeInImg(item); res.push({ code, date, score, highlight, item }); } if (!res.length) return res; if (isMovieList) { this.driveMatchForList(res, this.click.upQuery); addPrefetch(hlUrls); if (isMerge) { res.sort((pre, next) => { return pre.score === next.score ? next.date - pre.date : next.score - pre.score; }); } res.sort((pre, next) => (pre.highlight > next.highlight ? -1 : 1)); } return res.map(({ item }) => item); }, modifyMovieCard(item) { const titleNode = item.querySelector(".video-title"); const code = titleNode.querySelector("strong").textContent.trim(); const title = titleNode.textContent.replace(code, "").trim(); const score = item .querySelector(".score .value") .textContent.replace(/ /g, "") .trim(); const tags = item.querySelectorAll(".tags .tag"); const filterClass = this.listFilter({ code, title, score, tags }); if (filterClass) item.classList.add(filterClass); if (filterClass === "x-hide") return; titleNode.insertAdjacentHTML( "afterbegin", `<span class="x-btn" title="列表离线" data-code="${code}" data-title="${title}"></span>` ); if (item.querySelector(".tags")) titleNode.classList.add("x-ellipsis"); titleNode.classList.add("x-title"); }, }; movie = { info: {}, params: {}, magnets: [], docStart() { const style = ` .video-meta-panel { margin: -10px 0 20px; padding: 0; } .video-meta-panel > .columns { margin: 0; align-items: start; } .video-meta-panel > .columns > .column { padding: 0 10px; } .video-meta-panel > .columns > .column-video-cover { margin: 10px; padding: 0; aspect-ratio: var(--x-cover-ratio); } .column-video-cover a:has(> :is(img, video)), .column-video-cover .cover-container::after, .preview-video-container::after { width: 100%; height: 100%; } img, video { vertical-align: top; } :is(.column-video-cover, .tile-images) a:has(> :is(img, video)) { display: block; background: var(--x-sub-ftc); } .tile-images a:has(> img) { aspect-ratio: var(--x-thumb-ratio); } .preview-images a:has(> img) { aspect-ratio: var(--x-sprite-ratio); } :root[data-theme="dark"] :is(.column-video-cover, .tile-images) a:has(> :is(img, video)) { background: var(--x-grey) !important; } :is(.column-video-cover, .tile-images) :is(img, video) { width: 100% !important; height: 100% !important; max-height: unset !important; object-fit: contain; } .movie-panel-info div.panel-block { padding: 10px 0; font-size: 14px; line-height: var(--x-line-h); } :root[data-theme="dark"] .panel .panel-block:last-child { border-bottom: none; } .panel-block:has(#x-res) { padding-bottom: 0; border-bottom: 0; } .panel-block:has(#x-match) { border-bottom: 0; } .panel-block:has(#toolbar) { padding-top: 0; } .movie-panel-info .copy-to-clipboard { height: var(--x-line-h); } .video-detail > .columns { margin-bottom: 8px; } .message-header, .message-body, #magnets-content > .columns > .column { padding: 10px; } .video-panel .tile-images, .plain-grid-list { gap: 10px; } .video-panel .tile-images .tile-item .video-number { padding-top: 6px; } .columns[data-controller="movie-tab"], #magnets-content > .columns, .plain-grid-list a.box { margin: 0; } .columns[data-controller="movie-tab"] > .column, .video-panel .message-body .top-meta { padding: 0; } #tabs-container .message { margin-bottom: 20px !important; } .top-meta :is(.button.is-info, .moj-content) { display: none; } #reviews .review-items .review-item { padding: 10px 0; } #reviews .review-items .review-item:first-child { padding-top: 0; } #reviews .review-items .review-item:last-child { padding-bottom: 0; } .plain-grid-list .item, nav.pagination.no-line:has(a) { display: flex; } .plain-grid-list .item { align-items: baseline; } .plain-grid-list .item strong { flex: 1; padding-right: 10px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .column-video-cover .cover-container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .isPlayer .cover-container { top: 25%; } .column-video-cover .cover-container .play-button { position: unset; transform: none; } .column-video-cover a[id] { opacity: 0; position: absolute; top: 0; left: 0; } .column-video-cover a[id].active { z-index: 1; opacity: 1; transition: opacity .25s linear; } .column-video-cover video { display: inline !important; } #x-res, #toolbar { width: 100%; } :is(#x-res, #toolbar) button { flex: 1; } .tags:has(.x-jump) { margin-bottom: -5px; } html:has(.fancybox-is-open) { overflow: hidden; } .fancybox-slide--image.fancybox-slide--current { overflow: overlay; } .fancybox-slide--image.fancybox-slide--current .fancybox-content { left: 50%; width: fit-content !important; height: auto !important; padding: 44px 0; transform: translateX(-50%) !important; } .fancybox-slide--image.fancybox-slide--current .fancybox-content img { position: static; width: auto; max-width: calc(100vw - 140px); height: auto; } .top-meta:has(.tag) { margin-bottom: 10px; padding-right: 10px !important; } .top-meta .tags { flex: 1; margin: 0 0 -8px; } #magnets-content { ${this.M_LINE > 0 ? `max-height: calc(45px * ${this.M_LINE});` : ""} overscroll-behavior-y: contain; overflow: overlay; } :root[data-theme="dark"] #magnets-content::-webkit-scrollbar-thumb { background: #4a4a4a !important; } #magnets-content .item { height: 45px; border-bottom: 1px solid #ededed; } :root[data-theme=dark] #magnets-content .item { border-color: #4a4a4a; } #magnets-content .item:last-child { border: none; } #magnets-content .item .column:first-child { flex: 3; font-weight: bold; } #magnets-content .item .column:is(:nth-child(1), :nth-child(2), :nth-child(3)) { text-align: left; } #magnets-content .item .column.x-line { max-width: 180px; } #magnets-content .x-from { min-width: 65px; } #magnets-content .item .column:last-child { flex: none; text-align: right; } #x-total { padding: 10px 10px 0; text-align: right; } #toolbar button.active, #magnets-content a[data-magnet].active { opacity: .5; } `; this.globalDark(`${this._style.common}${style}`); this.listMerge(); }, async contentLoaded() { this.globalSearch(); this.modifyLayout(); const titleNode = DOC.querySelector("h2.title"); const currentTitle = titleNode.querySelector(".current-title"); const originTitle = titleNode.querySelector(".origin-title"); const code = DOC.querySelector(".first-block .value").textContent; const title = (originTitle ?? currentTitle).textContent.replace(code, "").trim(); this.info = { code: /^FC2-\d/i.test(code) ? code.replace("-", "-PPV-") : code, title, transTitle: originTitle ? currentTitle.textContent.trim() : "", cover: DOC.querySelector(".column-video-cover img").src, isVR: VR_REGEX.test(title), studio: "", series: "", genre: [], star: [], video: "", }; const video = DOC.querySelector("#preview-video source")?.getAttribute("src") ?? ""; if (video && (await request(video, {}, "HEAD"))) this.info.video = video; for (const item of DOC.querySelectorAll(".movie-panel-info > .panel-block")) { let [label, value] = item.querySelectorAll(["strong", ".value"]); if (!label || !value) continue; label = label?.textContent?.trim(); if (!label) continue; if (["片商:", "賣家:"].includes(label)) this.info.studio = value.textContent.trim(); if (label === "系列:") this.info.series = value.textContent.trim(); if (label === "評分:") item.classList.add("score"); if (label === "類別:") { item.classList.add("genre"); if (!value.querySelector("a")) continue; this.info.genre = Array.from(value.querySelectorAll("a")).map(item => item.textContent); } if (label === "演員:") { item.classList.add("star"); if (!value.querySelector("a")) continue; const male = []; const female = []; value.textContent .split("\n") .map(item => item.trim()) .filter(Boolean) .forEach(item => { (item.includes("♂") ? male : female).push(item.replace(/♂|♀/, "")); }); this.info.star = [...female, ...male]; } } this.getMovieResource(this.info); if (this.M_COPY) { titleNode.insertAdjacentHTML( "beforeend", ` <a class="copy-to-clipboard" title="原始标题" data-clipboard-text="${code} ${title}">复制</a>` ); GM_addStyle("#magnets-content a[data-copy]{display:inline-flex!important}"); DOC.querySelector(".panel-block.star .value").addEventListener("contextmenu", e => { const { target } = e; if (target.matches("a")) handleCopy(e, "", target.textContent); }); } this.movieRes(); this.movieTitle(); this._movieJump(); this.movieScore(); this.movieStar(); this.driveMatch(); this.refactorTable(); }, modifyLayout() { if (history.scrollRestoration) history.scrollRestoration = "manual"; window.scrollTo({ top: 150, left: 0, behavior: "smooth" }); this.modifyItem(); unsafeWindow.$.fancybox.defaults.loop = true; unsafeWindow.$.fancybox.defaults.smallBtn = false; unsafeWindow.$.fancybox.defaults.toolbar = true; unsafeWindow.$.fancybox.defaults.buttons = ["slideShow", "thumbs", "close"]; unsafeWindow.$.fancybox.defaults.animationEffect = "fade"; unsafeWindow.$.fancybox.defaults.animationDuration = 300; unsafeWindow.$.fancybox.defaults.transitionDuration = 150; unsafeWindow.$.fancybox.defaults.touch = { vertical: false }; unsafeWindow.$.fancybox.defaults.wheel = false; unsafeWindow.$.fancybox.defaults.clickContent = false; const cover = DOC.querySelector(".cover-container"); if (!cover) return; const _cover = cover.cloneNode(); const playBtn = cover.querySelector(".play-button"); _cover.append(playBtn.cloneNode(true)); playBtn.replaceWith(_cover); _cover.addEventListener("click", e => e.stopPropagation()); cover.removeAttribute("rel"); cover.removeAttribute("class"); cover.removeAttribute("target"); cover.href = cover.querySelector("img").src; cover.setAttribute("data-fancybox", "gallery"); }, modifyItem() { const selectors = [".tile-images.tile-small .tile-item"]; const codeQuery = ".video-number"; const upQuery = "a:has(> img)"; const res = []; for (const item of DOC.querySelectorAll(selectors)) { const img = item.querySelector("img"); if (!img) continue; img.replaceWith(DOC.create("a", { href: VOID }, img.cloneNode())); const code = item.querySelector(codeQuery)?.textContent; if (!code) continue; item.querySelector(".video-title")?.classList.add("x-title"); res.push({ item, code }); } this.driveMatchForList(res, upQuery); this.click.selectors = selectors; this.click.codeQuery = codeQuery; this.click.upQuery = upQuery; this.globalClick(); }, movieRes() { const { res } = this.params; if (!res?.length) return; const box = DOC.querySelector(".column-video-cover"); const info = DOC.querySelector(".movie-panel-info"); const first = "x-cover"; const firstQuery = `button[for=${first}]`; const cover = box.querySelector("a[data-fancybox]"); cover.setAttribute("data-fancybox", "tabbar"); cover.setAttribute("id", first); cover.classList.add("active"); box.insertAdjacentHTML( "afterbegin", '<div class="x-side prev"><span>🔙</span></div><div class="x-side next"><span>🔜</span></div>' ); info.insertAdjacentHTML( "afterbegin", `<div class="panel-block"> <div id="x-res" class="buttons has-addons are-small"> <button class="button is-light is-active" for="${first}">封面</button> ${res .map( item => `<button class="button is-light is-loading" for="x-${item}" disabled>暂无</button>` ) .join("")} </div> </div>` ); box.addEventListener("click", e => { const { target } = e; const side = target.closest(".x-side"); if (side) { e.stopPropagation(); e.preventDefault(); const navs = info.querySelectorAll("#x-res button:not([disabled])"); const { length } = navs; if (length === 1) return; let index = Array.from(navs).findIndex(item => item.classList.contains("is-active")); index = side.classList.contains("next") ? index + 1 : index - 1; if (index > length - 1) index = 0; if (index === -1) index = length - 1; return navs[index].click(); } if (target.closest(".x-player")) { e.stopPropagation(); e.preventDefault(); return box.querySelector(".x-side.next").click(); } }); info.querySelector("#x-res").addEventListener("click", ({ target }) => { const tag = target.getAttribute("for"); if (!tag) return; const active = box.querySelector(`#${tag}`); if (target.classList.contains("is-active")) return active.querySelector("video")?.focus(); const _target = info.querySelector("#x-res .is-active"); _target.classList.toggle("is-active"); if (_target.matches(firstQuery)) box.classList.remove("x-player"); target.classList.toggle("is-active"); if (target.matches(firstQuery) && box.classList.contains("isPlayer")) { box.classList.add("x-player"); } const _active = box.querySelector(".active"); _active.classList.toggle("active"); _active.querySelector("video")?.pause(); active.classList.toggle("active"); const video = active.querySelector("video"); if (!video) return; video.focus(); video.play(); }); res.forEach(key => { this.upMovieResource(key, val => { const id = `x-${key}`; const nav = info.querySelector(`button[for=${id}]`); nav.classList.remove("is-loading"); if (!val.length) return; this.info[key] = val; nav.removeAttribute("disabled"); nav.innerHTML = TAB_NAME[key]; const targetId = `${id}-target`; const node = key === "img" ? DOC.create(key, { src: val }) : createVideo(val, { id: targetId }); const container = DOC.create( "a", { "data-fancybox": "tabbar", id, href: key === "img" ? val : `#${targetId}` }, node ); box.append(container); if (key === "img" || !nav.previousElementSibling?.matches(firstQuery)) return; box.classList.add("isPlayer", "x-player"); }); }); }, movieTitle() { this.upMovieResource( "title", val => { DOC.querySelector("#x-title").textContent = val.length ? val : "暂无数据"; }, () => { DOC.querySelector(".panel-block.first-block").insertAdjacentHTML( "beforebegin", '<div class="panel-block"><strong>机翻:</strong> <span id="x-title" class="value">查询中...</span></div>' ); } ); }, _movieJump() { this.movieJump( this.info, res => { DOC.querySelector(".panel-block.first-block").insertAdjacentHTML( "afterend", res .map( ({ group, list }) => `<div class="panel-block"><strong>${group}:</strong> <div class="tags">${list .map( ({ url, query, name }) => `<span class="x-jump tag is-light${ query ? " is-info" : "" }" data-query="${query}" data-href="${url}">${name}</span>` ) .join("")}</div></div>` ) .join("") ); }, (res, { classList }) => { classList.remove("is-light"); if (res.zh) classList.replace("is-info", "is-warning"); } ); }, movieScore() { const { score } = this.params; if (!score?.length) return; const scoreTarget = DOC.querySelector(".panel-block.score"); const insertTarget = scoreTarget ?? DOC.querySelector(".panel-block.genre, .panel-block.star"); const insertScore = (position, item) => { insertTarget?.insertAdjacentHTML( position, `<div class="panel-block"><strong>${item.toUpperCase()}评分:</strong> <span id="x-${item}_score" class="value">查询中...</span></div>` ); }; let position = "beforebegin"; for (const item of score) { if (item === "db") { if (scoreTarget) { scoreTarget.querySelector("strong").innerHTML = "DB评分:"; position = "afterend"; } continue; } insertScore(position, item); } score .filter(key => key !== "db") .forEach(key => { key = `${key}_score`; this.upMovieResource(key, val => { const node = DOC.querySelector(`#x-${key}`); if (!val.length) { node.textContent = "暂无数据"; return; } const { score, total, num } = val[0]; let stars = Math.floor((score / total) * 5); stars = Array(5).fill("", 0, stars).fill(" gray", stars, 5); node.innerHTML = `<span class="score-stars">${stars .map(item => `<i class="icon-star${item}"></i>`) .join("")}</span> ${score}分${num ? `, 由${num}人评价` : ""}`; }); }); }, movieStar() { const target = DOC.querySelector(".panel-block.star .value"); this.upMovieResource( "star", val => { this.info.star = val; target.innerHTML = !val.length ? "暂无数据" : val .map( item => `<a href="/search?f=actor&q=${item}">${item}</a><strong class="symbol female">♀</strong>` ) .join(" "); }, () => { target.innerHTML = "查询中..."; } ); }, refactorTable() { let caption = DOC.querySelector(".top-meta"); caption.classList.add("is-flex"); caption.insertAdjacentHTML("afterbegin", '<div class="tags"></div>'); caption = caption.querySelector(".tags"); const table = DOC.querySelector("#magnets-content"); table.parentElement.insertAdjacentHTML("beforeend", '<div id="x-total">总数 0</div>'); const magnets = []; for (const item of table.querySelectorAll(".item")) { const first = item.querySelector("a"); if (!first) continue; const size = first.querySelector(".meta")?.textContent?.trim() ?? ""; magnets.push({ name: first.querySelector(".name").textContent, link: first.href.split("&")[0], size, bytes: transToBytes(size), zh: !!first.querySelector(".tag.is-warning.is-small.is-light"), date: item.querySelector(".time")?.textContent ?? "", }); } table.innerHTML = "暂无数据"; this.refactorTbody(magnets); (this.params?.magnet ?? []).forEach(key => { key = `${key}_magnet`; this.upMovieResource( key, res => this.refactorTbody(res, key), () => { caption.insertAdjacentHTML( "beforeend", `<span class="tag is-success is-light" id="x-${key}">${key .split("_")[0] .toUpperCase()}搜索</span>` ); } ); }); }, refactorTbody(magnets, key) { if (!magnets.length) return; if (key) DOC.querySelector(`#x-${key}`)?.classList.remove("is-light"); let start; if (this.magnets.length) { for (const item of magnets) { const { link, zh } = item; const index = this.magnets.findIndex(item => item.link.toLowerCase() === link.toLowerCase()); if (index === -1) { this.magnets.push(item); continue; } if (zh) this.magnets[index].zh = zh; } magnets = this.magnets; } else { const caption = DOC.querySelector(".top-meta"); start = () => { caption .querySelector(".tags") .insertAdjacentHTML("beforeend", '<span class="tag is-success">磁力排序</span>'); }; if (this.M_COPY) { caption.insertAdjacentHTML( "beforeend", `<a href="${VOID}" data-copy="all" title="复制全部磁链" class="tag is-info">复制全部</a>` ); } DOC.querySelector("#magnets .message-body").addEventListener("click", e => { const { copy, magnet } = e.target.dataset; if (!copy && !magnet) return; if (magnet) return this._driveOffLine(e); if (copy !== "all") return handleCopy(e); handleCopy(e, "", this.magnets.map(item => item.link).join("\n")); }); } const total = DOC.querySelector("#x-total"); if (total) total.textContent = `总数 ${magnets.length}`; magnets = this.movieSort(magnets, start); this.magnets = magnets; DOC.querySelector("#magnets-content").innerHTML = this.refactorTr(magnets); }, refactorTr(magnets) { return magnets .map( ({ name, link, size, date, from, href, zh }) => `<div class="item columns odd"> <div class="column" title="${name}"> <a href="${link}" class="x-ellipsis">${name}</a> </div> <div class="column x-line" title="${size}">${size}</div> <div class="column x-line" title="${date}">${date}</div> <div class="column"> <a class="tag is-danger is-light x-from" href="${href || VOID}" ${href ? ' target="_blank" title="查看详情"' : ""} >${from || Domain}</a> </div> <div class="column"> ${zh ? '<span class="tag is-warning is-light">字幕</span>' : ""} </div> <div class="column"> <a href="${VOID}" class="tag is-info is-hidden" title="复制磁力链接" data-copy="${link}" >复制链接</a><a href="${VOID}" class="tag is-info x-ml is-hidden" title="添加离线任务" data-magnet="${link}" >添加离线</a> </div> </div>` ) .join(""); }, driveMatch() { this.driveMatchForMovie( this.info, res => { const refresh = DOC.querySelector("#x-refresh"); refresh.inert = false; refresh.textContent = "刷新资源"; refresh.classList.remove("active"); DOC.querySelector("#x-match").innerHTML = !res.length ? "暂无数据" : res .map( ({ n, pc, fid, cid, t }) => `<a class="x-ellipsis" href="${VOID}" data-n="${n}" data-pc="${pc}" data-fid="${fid}" data-cid="${cid}" title="[${t}] ${n}"><span class="x-btn" title="资源调整"></span>${n}</a>` ) .join(""); }, () => { const refresh = DOC.querySelector("#x-refresh"); if (refresh) { refresh.inert = true; refresh.textContent = "请求中..."; return refresh.classList.add("active"); } const { icon, resources } = GM_info.script; addPrefetch([icon, ...resources.map(item => item.url)]); GM_addStyle("#magnets-content a[data-magnet]{display:inline-flex!important}"); DOC.querySelector(".movie-panel-info").insertAdjacentHTML( "beforeend", '<div class="panel-block"><strong>资源:</strong> <span class="value" id="x-match">查询中...</span></div><div class="panel-block"><div id="toolbar" class="buttons has-addons are-small"><button class="button is-info" id="x-magnet" data-magnet="all">一键离线</button><button class="button is-info" id="x-refresh" inert>请求中...</button></div></div>' ); DOC.querySelector("#x-refresh").addEventListener("click", () => this.driveMatch()); DOC.querySelector("#x-magnet").addEventListener("click", e => this._driveOffLine(e)); const modify = this.upModifyTarget(); DOC.querySelector("#x-match").addEventListener("click", e => { if (e.target.dataset.pc) this.advancedLink(e, { type: "left" }); if (e.target.classList.contains("x-btn")) this.driveModify(e, modify); }); DOC.querySelector("#x-match").addEventListener("contextmenu", e => { if (e.target.dataset.cid) this.advancedLink(e, { type: "right" }); }); } ); }, _driveOffLine(e) { this.driveOffLine(e, { ...this.info, magnets: this.magnets }); }, }; others = { docStart() { this.globalDark(this._style.common); this.listMerge(); }, contentLoaded() { this.globalSearch(); }, }; } class JavBus extends Common { constructor() { super(); return super.init(); } routes = { list: /^\/((uncensored\/?)?(page\/\d+)?$)|((uncensored\/)?(((search|star)+|genre|studio|label|series|director|member)+\/)|actresses(\/\d+)?)+/i, genre: /^\/(uncensored\/)?genre$/i, forum: /^\/forum\//i, movie: /^\/[a-z0-9]+(-|\w)+/i, }; _style = { common: ` body { overflow-y: overlay; } .ad-box, footer { display: none; } `, card: ` a:is(.movie-box, .avatar-box) { width: var(--x-thumb-w); margin: 10px !important; } .photo-frame { height: auto !important; margin: 10px !important; background: var(--x-sub-ftc); border: none; } .movie-box .photo-frame { aspect-ratio: var(--x-thumb-ratio); } .avatar-box .photo-frame { aspect-ratio: var(--x-avatar-ratio); } .photo-frame img { width: 100%; max-width: none !important; height: 100% !important; max-height: none !important; margin: 0 !important; object-fit: cover; } .photo-info { height: auto !important; padding: 0 10px 10px; line-height: var(--x-line-h); background: unset; border: none; } `, dark: ` :is(.nav, .dropdown-menu) > li > a:is(:hover, :focus) { background: var(--x-grey) !important; } .nav > :is(li.active, .open) > a, .nav > .open > a:is(:hover, :focus), .dropdown-menu { background: var(--x-bgc) !important; } .modal-content, .alert { background: var(--x-sub-bgc) !important; } .btn-primary { background: var(--x-blue) !important; border: none; } .btn-success { background: var(--x-green) !important; border: none; } .btn-warning { background: var(--x-orange) !important; border: none; } .btn-danger { background: var(--x-red) !important; border: none; } .btn-link { background: none !important; border: none; } .btn.disabled, .btn[disabled], fieldset[disabled] .btn { opacity: .8 !important; } `, dmCard: ` .movie-box, .avatar-box { background: var(--x-sub-bgc) !important; } .photo-frame { background: var(--x-grey); } .photo-info { color: unset; } .photo-info span date { color: var(--x-sub-ftc); } `, }; search = { selectors: "#search-input", pathname: `/search/${REP}`, }; click = { selectors: [".movie-box", ".avatar-box"], codeQuery: "date", upQuery: ".photo-frame", }; merge = { start: (list, active) => { if (active) { active = " active"; DOC.querySelector("#navbar .active").classList.remove("active"); } DOC.querySelector("#navbar > .nav.navbar-nav")?.insertAdjacentHTML( "beforeend", `<li id="merge" class="dropdown hidden-sm${active}"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" role="button" aria-expanded="false" >合并列表 <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> ${list.reduce((prev, curr) => `${prev}<li><a href="/?merge=${curr}">${curr}</a></li>`, "")} </ul> </li>` ); }, }; list = { imgReplace: [ { regex: /\/thumbs?\//i, replace: val => val.replace(/\/thumbs?\//g, "/cover/").replace(".jpg", "_b.jpg"), }, { regex: /pics\.dmm\.co\.jp/i, replace: val => val.replace("ps.jpg", "pl.jpg"), }, ], docStart() { const { common, card, dark, dmCard } = this._style; const style = ` .alert-common, .alert-page { margin-top: 20px; } .search-header { padding: 0; background: none; box-shadow: none; } .search-header .nav-tabs, #waterfall { display: none; } #waterfall, #waterfall img { opacity: 0; } .text-center.hidden-xs { display: none; line-height: 0; } .pagination { margin-bottom: 40px; } .movie-box .x-title + div { height: var(--x-line-h) !important; margin: 4px 0; } .mleft { display: flex !important; align-items: center; } .mleft .btn-xs { margin: 0 6px 0 0 !important; } `; const dmStyle = ` .nav-pills > li.active > a { background-color: var(--x-blue) !important; } .pagination > li > a { color: var(--x-ftc) !important; background-color: var(--x-sub-bgc) !important; } .pagination > li:not(.active) > a:hover { background-color: var(--x-grey) !important; } `; this.globalDark(`${common}${style}${card}${this.listMovieTitle()}`, `${dark}${dmStyle}${dmCard}`); this.listMerge(); this.upLOLTarget(); }, contentLoaded() { this.globalSearch(); this.globalClick(); this.modifyLayout(); }, modifyLayout() { DOC.querySelector(".search-header .nav")?.classList.replace("nav-tabs", "nav-pills"); const container = unsafeWindow.$("#waterfall"); this.listScroll({ container: "#waterfall", path: "#next", start: items => container.empty().append(items).show().masonry("reload"), showPage: () => DOC.querySelector(".text-center.hidden-xs")?.classList.add("x-show"), onLoad: items => { items = unsafeWindow.$(items); container.append(items).masonry("appended", items); }, }); }, modifyItem(items, isMerge) { const isMovieList = items.some(item => item.querySelector(".movie-box")); const res = []; const hlUrls = []; for (let index = 0, { length } = items; index < length; index++) { const item = items[index]; let code = ""; let date = ""; let highlight = false; if (isMovieList) { [code, date] = item.querySelectorAll("date"); if (!code || !date) continue; code = code?.textContent.trim(); date = date?.textContent.replaceAll("-", "").trim(); if (!code || !date) continue; if (isMerge && res.some(t => t.code === code && t.date === date)) continue; this.modifyMovieCard(item); if (item.querySelector(".x-hide")) continue; highlight = item.querySelector(".x-highlight"); if (highlight) hlUrls.push(highlight.href); } else { this.modifyAvatarCard(item); } fadeInImg(item); res.push({ code, date, highlight: !!highlight, item }); } if (!res.length) return res; if (isMovieList) { this.driveMatchForList(res, this.click.upQuery); addPrefetch(hlUrls); if (isMerge) res.sort((pre, next) => next.date - pre.date); res.sort((pre, next) => (pre.highlight > next.highlight ? -1 : 1)); } return res.map(({ item }) => item); }, modifyMovieCard(item) { item = item.querySelector(".movie-box"); if (!item) return; const info = item.querySelector(".photo-info span"); info.querySelector(".__cf_email__")?.remove(); info.innerHTML = info.innerHTML.replace("<br>", ""); const titleNode = info.firstChild; const title = titleNode.textContent.trim(); const code = info.querySelector("date").textContent.trim(); const tags = info.querySelectorAll(".item-tag button"); const filterClass = this.listFilter({ code, title, tags }); if (filterClass) item.classList.add(filterClass); if (filterClass === "x-hide") return; this.listMovieImgType(item, this.imgReplace); const reTitle = DOC.create("div", { title, class: "x-ellipsis x-title" }, title); reTitle.insertAdjacentHTML( "afterbegin", `<span class="x-btn" title="列表离线" data-code="${code}" data-title="${title}"></span>` ); titleNode.replaceWith(reTitle); }, modifyAvatarCard(item) { item = item.querySelector(".avatar-box"); if (!item) return; const info = item.querySelector("span"); if (!info.classList.contains("mleft")) return info.classList.add("x-line"); const titleNode = info.firstChild; const title = titleNode.textContent.trim(); titleNode.replaceWith(DOC.create("div", { title, class: "x-line" }, title)); info.insertAdjacentElement("afterbegin", info.querySelector("button")); }, }; genre = { docStart() { const { common, dark } = this._style; const style = ":root{accent-color:var(--x-blue)}.container-fluid{padding:0 20px!important}.alert-common{margin:20px 0 0!important}h4{margin:20px 0 10px 0!important}.genre-box{margin:10px 0 20px 0!important;padding:20px!important}.genre-box a{text-align:left!important;cursor:pointer!important;user-select:none!important}.genre-box input{margin:0 4px 0 0!important;vertical-align:middle!important}.x-last-box{margin-bottom:70px!important}button.btn.btn-danger.btn-block.btn-genre{position:fixed!important;bottom:0!important;left:0!important;margin:0!important;border:none!important;border-radius:0!important}"; const dmStyle = ".genre-box{background:var(--x-sub-bgc)!important}"; this.globalDark(`${common}${style}`, `${dark}${dmStyle}`); this.listMerge(); }, contentLoaded() { this.globalSearch(); if (!DOC.querySelector("button.btn.btn-danger.btn-block.btn-genre")) return; const box = DOC.querySelectorAll(".genre-box"); box[box.length - 1].classList.add("x-last-box"); DOC.querySelector(".container-fluid.pt10").addEventListener("click", ({ target }) => { if (target.nodeName !== "A" || !target.classList.contains("text-center")) return; const checkbox = target.querySelector("input"); checkbox.checked = !checkbox.checked; }); }, }; forum = { docStart() { const style = "#nv_search #ft,.banner300,.banner728,.bcpic,.bcpic2,.jav-footer{display:none!important}#toptb{position:fixed!important;top:0!important;right:0!important;left:0!important;z-index:999!important;border-color:#e7e7e7;box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}#search-input{border-right:none!important}.jav-button{margin-top:-3px!important;margin-left:-4px!important}#wp{margin-top:55px!important}#ct:has(#scform){margin-top:38px}.biaoqicn_show a{width:20px!important;height:20px!important;line-height:20px!important;opacity:.7;user-select:none!important}#online .bm_h{border-bottom:none!important}#online .bm_h .o img{margin-top:48%}#moquu_top{right:20px;bottom:20px;box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}"; const ifDark = this.G_DARK ? "::-webkit-scrollbar-thumb{background:var(--x-grey)!important}*{box-shadow:none!important;font-weight:600}#x-mask,html,img{filter:invert(1) hue-rotate(.5turn)}img{opacity:.85}" : ""; this.globalDark(`${this._style.common}${style}${ifDark}`); this.merge.start = list => { DOC.querySelector("#toptb ul")?.insertAdjacentHTML( "beforeend", `<li class="nav-title nav-inactive"><a href="/?merge=${list[0]}">合并列表</a></li>` ); }; this.listMerge(); }, contentLoaded() { this.globalSearch(); }, }; movie = { info: {}, params: {}, magnets: [], docStart() { const { common, card, dark, dmCard } = this._style; const style = ` #mag-submit-show, #mag-submit, #magnet-table, h4[style="position:relative"], h4[style="position:relative"] + .row, .info .glyphicon-info-sign { display: none !important; } html { padding-right: 0 !important; } .container { margin-bottom: 40px; } @media (width >= 1270px) { .container { width: 1270px; } } .row.movie { padding: 10px 0; } @media (width <= 480px) { .row.movie { padding: 0 !important; } } .screencap, .info { border: none !important; padding: 0 10px; } .bigImage { position: relative; overflow: hidden; display: block; background: var(--x-sub-ftc); aspect-ratio: var(--x-cover-ratio); } .bigImage :is(img, video) { width: 100%; height: 100%; object-fit: contain; } .bigImage [id] { opacity: 0; position: absolute; top: 0; left: 0; } .bigImage .active { z-index: 1; opacity: 1 !important; transition: opacity .25s linear; } @media (width <= 992px) { .info { padding-top: 10px !important; } } .info p { line-height: var(--x-line-h) !important; } .info :is(.header, .star-show, ul) { margin-bottom: 0; } .info :is(.glyphicon-plus, .glyphicon-minus) { margin-left: 4px; font-size: 13px; } .star-box li { background: var(--x-sub-ftc) !important; } .star-box img { width: 100% !important; height: 100% !important; aspect-ratio: var(--x-avatar-ratio) !important; font-size: 0; margin: 0 !important; } .star-box .star-name { padding: 6px 4px !important; background: #fff; border: none !important; } .star-box .star-name, :is(.avatar-box, .movie-box) span { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } #magneturlpost + .movie { margin-top: 20px !important; padding: 10px !important; } #avatar-waterfall, #sample-waterfall, #related-waterfall { margin: -10px !important; word-spacing: -20px; } .avatar-box, .sample-box, .movie-box { vertical-align: top !important; word-spacing: 0 !important; } .avatar-box span { padding-top: 0 !important; background-color: unset !important; border: none !important; } .sample-box { margin: 10px !important; width: var(--x-thumb-w) !important; } .sample-box .photo-frame { aspect-ratio: var(--x-sprite-ratio); } .is-loading { animation: spinAround .7s linear infinite; } @keyframes spinAround { 0% { transform: rotate(0deg) } to { transform: rotate(359deg) } } .x-star { color: var(--x-orange); } .x-table { margin: 0 !important; } .x-caption .label { position: unset !important; } .x-table tr { display: table; width: 100%; table-layout: fixed; } .x-table tr > * { vertical-align: middle !important; border-left: none !important; } .x-table tr > *:first-child { width: 50px; } .x-table tr > *:nth-child(2) { width: 33.3%; } .x-table tr > *:last-child, .x-table tfoot tr > th:not(:nth-child(3)) { border-right: none !important; } .x-table tbody { display: block; ${this.M_LINE > 0 ? `max-height: calc(39px * ${this.M_LINE} - 1px);` : ""} overscroll-behavior-y: contain; overflow: overlay; table-layout: fixed; } .x-table tbody tr > * { border-top: none !important; } .x-table tbody tr:last-child > *, .x-table tfoot tr > * { border-bottom: none !important; } .x-table code { display: inline-block; min-width: 65px; } .x-table td a:not(:last-child) { margin-right: 10px; } #x-match a { color: #CC0000 !important; } `; const dmStyle = ` .bigImage, .btn-group a, .star-box li, tbody tr:hover, .table-striped > tbody > tr:nth-of-type(odd):hover, .x-table code { background: var(--x-grey) !important; } .btn-group a.active { background: var(--x-bgc) !important; } .movie, .btn-group a[disabled], .star-box .star-name, .sample-box, .table-striped > tbody > tr:nth-of-type(odd) { background: var(--x-sub-bgc) !important; } .avatar-box span { color: unset !important; } `; this.globalDark(`${common}${style}${card}`, `${dark}${dmStyle}${dmCard}`); this.listMerge(); }, contentLoaded() { this.globalSearch(); this.modifyItem(); const infoNode = DOC.querySelector(".info"); const info = infoNode.textContent; const codeNode = infoNode.querySelector("span[style='color:#CC0000;']"); const code = codeNode.textContent; const titleNode = DOC.querySelector("h3"); const title = titleNode.textContent.replace(code, "").trim(); this.info = { code, title, cover: DOC.querySelector(".bigImage img").src, isVR: VR_REGEX.test(title), studio: info.match(/(?<=製作商: ).+/g)?.pop(0), star: Array.from(infoNode.querySelectorAll("ul + p a")).map(item => item.textContent), series: info.match(/(?<=系列: ).+/g)?.pop(0), genre: Array.from(infoNode.querySelectorAll("p.header + p a")).map(item => item.textContent), }; const cid = DOC.querySelector("#sample-waterfall a.sample-box")?.href; if (cid?.includes("pics.dmm.co.jp")) this.info.cid = cid.split("/").at(-2); this.getMovieResource(this.info); if (this.M_COPY) { addCopy(titleNode, { title: "复制标题" }); addCopy(codeNode, { title: "复制番号" }); GM_addStyle("tbody a[data-copy]{display:inline!important}"); infoNode.addEventListener("contextmenu", e => { const { target } = e; if (!target.closest("#x-star") || !target.matches("a")) return; handleCopy(e, "", target.textContent); }); } this.movieRes(); this.movieTitle(); this._movieJump(); this.movieScore(); this.movieStar(); this.driveMatch(); const tableObs = new MutationObserver((_, obs) => { obs.disconnect(); this.refactorTable(); }); tableObs.observe(DOC.querySelector("#movie-loading"), { attributeFilter: ["style"] }); }, modifyItem() { const container = DOC.querySelector("#related-waterfall"); if (!container) return; const res = []; for (const item of container.querySelectorAll(".movie-box")) { const code = item.href?.split("/").at(-1); if (!CODE_REGEX.test(code)) continue; item.append(DOC.create("date", { class: "x-hide" }, code)); item.querySelector(".photo-info span").classList.add("x-title"); res.push({ item, code }); } this.driveMatchForList(res, ".photo-frame"); this.globalClick(); }, movieRes() { const { res } = this.params; if (!res?.length) return; unsafeWindow.$(".bigImage").magnificPopup({ type: "image", closeOnContentClick: true, closeBtnInside: false, image: { verticalFit: false }, }); const box = DOC.querySelector(".bigImage"); const info = DOC.querySelector(".info"); const first = "x-cover"; const firstQuery = `a[for=${first}]`; const cover = box.querySelector("img"); cover.setAttribute("id", first); cover.classList.add("active"); box.insertAdjacentHTML( "afterbegin", '<div class="x-side prev"><span class="glyphicon glyphicon-chevron-left"></div><div class="x-side next"><span class="glyphicon glyphicon-chevron-right"></div>' ); info.insertAdjacentHTML( "afterbegin", `<div id="x-res" class="btn-group btn-group-sm btn-group-justified mb10"> <a href="${VOID}" class="btn btn-default active" role="button" for="${first}">封面</a> ${res .map( item => `<a href="${VOID}" class="btn btn-default" role="button" for="x-${item}" disabled><span class="glyphicon glyphicon-repeat is-loading" aria-hidden="true"></span></a>` ) .join("")} </div>` ); box.addEventListener( "click", e => { const { target } = e; const side = target.closest(".x-side"); if (side) { e.stopPropagation(); e.preventDefault(); const navs = info.querySelectorAll("#x-res a:not([disabled])"); const { length } = navs; if (length === 1) return; let index = Array.from(navs).findIndex(item => item.classList.contains("active")); index = side.classList.contains("next") ? index + 1 : index - 1; if (index > length - 1) index = 0; if (index === -1) index = length - 1; return navs[index].click(); } if (target.closest(".x-player")) { e.stopPropagation(); e.preventDefault(); return box.querySelector(".x-side.next").click(); } const { nodeName } = target; if (nodeName === "IMG") box.href = target.src; if (nodeName !== "VIDEO") return; e.stopPropagation(); e.preventDefault(); target.paused ? target.play() : target.pause(); }, true ); info.querySelector("#x-res").addEventListener("click", ({ target }) => { const tag = target.getAttribute("for"); if (!tag) return; const active = box.querySelector(`#${tag}`); if (target.classList.contains("active")) return active.focus(); const _target = info.querySelector("#x-res .active"); _target.classList.toggle("active"); if (_target.matches(firstQuery)) box.classList.remove("x-player"); target.classList.toggle("active"); if (target.matches(firstQuery) && box.classList.contains("isPlayer")) { box.classList.add("x-player"); } const _active = box.querySelector(".active"); _active.classList.toggle("active"); if (_active.nodeName === "VIDEO") _active.pause(); active.classList.toggle("active"); if (active.nodeName !== "VIDEO") return; active.focus(); active.play(); }); res.forEach(key => { this.upMovieResource(key, val => { const id = `x-${key}`; const nav = info.querySelector(`a[for=${id}]`); if (!val.length) { nav.innerHTML = "暂无"; return; } this.info[key] = val; nav.removeAttribute("disabled"); nav.innerHTML = TAB_NAME[key]; const node = key === "img" ? DOC.create(key, { src: val, id }) : createVideo(val, { id }); box.append(node); if (key === "img" || !nav.previousElementSibling?.matches(firstQuery)) return; box.classList.add("isPlayer", "x-player"); }); }); }, movieTitle() { this.upMovieResource( "title", val => { DOC.querySelector("#x-title").textContent = val.length ? val : "暂无数据"; }, () => { DOC.querySelector("span[style='color:#CC0000;']").parentElement.insertAdjacentHTML( "beforebegin", '<p><span class="header">机翻标题:</span> <span id="x-title">查询中...</span></p>' ); } ); }, _movieJump() { this.movieJump( this.info, res => { DOC.querySelector("span[style='color:#CC0000;']").parentElement.insertAdjacentHTML( "afterend", res .map( ({ group, list }) => `<p><span class="header">${group}:</span> ${list .map( ({ url, query, name }) => `<button type="button" class="x-jump btn ${ query ? "btn-default" : "btn-link" } btn-xs" data-query="${query}" data-href="${url}">${name}</button>` ) .join("")}</p>` ) .join("") ); }, (res, node) => node.classList.replace("btn-default", res.zh ? "btn-warning" : "btn-primary") ); }, movieScore() { const { score } = this.params; if (!score?.length) return; DOC.querySelector("p.header, p.star-show").insertAdjacentHTML( "beforebegin", score .map( item => `<p><span class="header">${item.toUpperCase()}评分:</span> <span id="x-${item}_score">查询中...</span></p>` ) .join("") ); score.forEach(key => { key = `${key}_score`; this.upMovieResource(key, val => { const node = DOC.querySelector(`#x-${key}`); if (!val.length) { node.textContent = "暂无数据"; return; } const { score, total, num } = val[0]; let stars = Math.floor((score / total) * 5); stars = Array(5).fill(" x-star", 0, stars).fill("", stars, 5); node.innerHTML = `${stars .map(item => `<span class="glyphicon glyphicon-star${item}"></span>`) .join("")} ${score}分${num ? `, ${num}人评价` : ""}`; }); }); }, movieStar() { const noStar = DOC.querySelector(".glyphicon-info-sign"); if (!noStar) return DOC.querySelector(".info ul + p").setAttribute("id", "x-star"); noStar.nextSibling.replaceWith(DOC.create("p", { id: "x-star" }, "暫無出演者資訊")); this.upMovieResource( "star", val => { this.info.star = val; DOC.querySelector("#x-star").innerHTML = !val.length ? "暂无数据" : val .map(item => `<span class="genre"><a href="/searchstar/${item}">${item}</a></span>`) .join(""); }, () => { DOC.querySelector("#x-star").innerHTML = "查询中..."; } ); }, refactorTable() { const table = DOC.querySelector("#magnet-table"); const keys = (this.params?.magnet ?? []).map(item => `${item}_magnet`); const magnets = []; if (this.params.hasOwnProperty("sub")) { keys.unshift("sub"); } else { for (const tr of table.querySelectorAll("tr")) { let [first, size, date] = tr.querySelectorAll("td"); if (!first || !size || !date) continue; const link = first.querySelector("a"); if (!link?.href) continue; size = size?.textContent?.trim() ?? ""; magnets.push({ name: link?.textContent?.trim() ?? "", link: link.href.split("&")[0], zh: !!first.querySelector("a.btn.btn-mini-new.btn-warning.disabled"), size, bytes: transToBytes(size), date: date?.textContent?.trim() ?? "", }); } } table.parentElement.innerHTML = ` <table class="table table-striped table-hover table-bordered x-table"> <caption><div class="x-flex-center x-caption">重构的表格</div></caption> <thead> <tr> <th scope="col">#</th> <th scope="col">磁力名称</th> <th scope="col">档案大小</th> <th scope="col">分享日期</th> <th scope="col" class="text-center">来源</th> <th scope="col" class="text-center">字幕</th> <th scope="col">操作</th> </tr> </thead> <tbody> <tr><th scope="row" colspan="7" class="text-center text-muted">暂无数据</th></tr> </tbody> <tfoot> <tr> <th scope="row"></th> <th></th> <th colspan="4" class="text-right">总数</th> <td>0</td> </tr> </tfoot> </table>`; const caption = DOC.querySelector(".x-table .x-caption"); this.refactorTbody(magnets); keys.forEach(key => { this.upMovieResource( key, res => this.refactorTbody(res, key), () => { caption.insertAdjacentHTML( "beforeend", `<span class="label label-default" id="x-${key}"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> ${ key === "sub" ? "字幕筛选" : `${key.split("_")[0].toUpperCase()}搜索` }</span>` ); } ); }); }, refactorTbody(magnets, key) { if (!magnets.length) return; const table = DOC.querySelector(".x-table"); if (key) table.querySelector(`#x-${key}`).classList.replace("label-default", "label-success"); let start; if (this.magnets.length) { for (const item of magnets) { const { link, zh } = item; const index = this.magnets.findIndex(item => item.link.toLowerCase() === link.toLowerCase()); if (index === -1) { this.magnets.push(item); continue; } if (zh) this.magnets[index].zh = zh; } magnets = this.magnets; } else { start = () => { table .querySelector(".x-caption") .insertAdjacentHTML( "beforeend", '<span class="label label-success" id="x-sort"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> 磁力排序</span>' ); }; if (this.M_COPY) { table.querySelector( "thead th:last-child" ).innerHTML = `<a href="${VOID}" data-copy="all" title="复制全部磁链">复制全部</a>`; } table.addEventListener("click", e => { const { copy, magnet } = e.target.dataset; if (!copy && !magnet) return; if (magnet) return this._driveOffLine(e); if (copy !== "all") return handleCopy(e); handleCopy(e, "", this.magnets.map(item => item.link).join("\n")); }); } table.querySelector("tfoot td").textContent = magnets.length; magnets = this.movieSort(magnets, start); this.magnets = magnets; table.querySelector("tbody").innerHTML = this.refactorTr(magnets); }, refactorTr(magnets) { return magnets .map( ({ name, link, size, date, from, href, zh }, index) => ` <tr> <th scope="row">${++index}</th> <th class="x-line" title="${name}"> <a href="${link}">${name}</a> </th> <td>${size}</td> <td>${date}</td> <td class="text-center"> <a href="${href || VOID}"${href ? ` target="_blank" title="查看详情"` : ""}> <code>${from || Domain}</code> </a> </td> <td class="text-center"> <span class="glyphicon glyphicon-${zh ? "ok" : "remove"}-circle text-${ zh ? "success" : "danger" }"></span> </td> <td> <a hidden href="${VOID}" data-copy="${link}" title="复制磁力链接">复制磁链</a><a hidden href="${VOID}" data-magnet="${link}" class="text-success" title="添加离线任务">添加离线</a> </td> </tr>` ) .join(""); }, driveMatch() { this.driveMatchForMovie( this.info, res => { const refresh = DOC.querySelector("#x-refresh"); refresh.inert = false; refresh.textContent = "刷新资源"; refresh.classList.remove("active"); DOC.querySelector("#x-match").innerHTML = !res.length ? "暂无数据" : res .map( ({ n, pc, fid, cid, t }) => `<a class="show x-line" href="${VOID}" data-n="${n}" data-pc="${pc}" data-fid="${fid}" data-cid="${cid}" title="[${t}] ${n}"><span class="x-btn" title="资源调整"></span>${n}</a>` ) .join(""); }, () => { const refresh = DOC.querySelector("#x-refresh"); if (refresh) { refresh.inert = true; refresh.textContent = "请求中..."; return refresh.classList.add("active"); } const { icon, resources } = GM_info.script; addPrefetch([icon, ...resources.map(item => item.url)]); GM_addStyle("tbody a[data-magnet]{display:inline!important}"); DOC.querySelector(".info").insertAdjacentHTML( "beforeend", `<p class="header">网盘资源:</p><p id="x-match">查询中...</p><div class="btn-group btn-group-sm btn-group-justified"><a href="${VOID}" class="btn btn-default" role="button" id="x-magnet" data-magnet="all">一键离线</a><a href="${VOID}" class="btn btn-default active" role="button" id="x-refresh" inert>请求中...</a></div>` ); DOC.querySelector("#x-refresh").addEventListener("click", () => this.driveMatch()); DOC.querySelector("#x-magnet").addEventListener("click", e => this._driveOffLine(e)); const modify = this.upModifyTarget(); DOC.querySelector("#x-match").addEventListener("click", e => { if (e.target.dataset.pc) this.advancedLink(e, { type: "left" }); if (e.target.classList.contains("x-btn")) this.driveModify(e, modify); }); DOC.querySelector("#x-match").addEventListener("contextmenu", e => { if (e.target.dataset.cid) this.advancedLink(e, { type: "right" }); }); } ); }, _driveOffLine(e) { this.driveOffLine(e, { ...this.info, magnets: this.magnets }); }, }; } class Drive115 { beforeUnload_time = 0; docStart() { Store.setVerifyStatus("pending"); unsafeWindow.onbeforeunload = () => { this.beforeUnload_time = new Date().getTime(); }; unsafeWindow.onunload = () => { if (new Date().getTime() - this.beforeUnload_time > 5) return; Store.setVerifyStatus("failed"); }; } contentLoaded() { unsafeWindow.focus(); DOC.querySelector("#js_ver_code_box button[rel=verify]").addEventListener("click", () => { const interval = setInterval(() => { if (DOC.querySelector(".vcode-hint").getAttribute("style").indexOf("none") !== -1) { Store.setVerifyStatus("verified"); unsafeWindow.onbeforeunload = null; unsafeWindow.onunload = null; clearTimer(); unsafeWindow.open("", "_self"); unsafeWindow.close(); } }, 300); const timeout = setTimeout(() => clearTimer(), 600); const clearTimer = () => { clearInterval(interval); clearTimeout(timeout); }; }); } } try { const Process = eval(`new ${Domain}()`); Process.docStart?.(); DOC.addEventListener("XContentLoaded", () => Process.contentLoaded?.(), { once: true }); } catch (err) { console.error(`${GM_info.script.name}: 无匹配模块`); } })();