JavScript

一站式体验,JavBus & JavDB 兼容

Fra 16.12.2023. Se den seneste versjonen.

  1. // ==UserScript==
  2. // @name JavScript
  3. // @namespace JavScript@blc
  4. // @version 4.5.5
  5. // @author blc
  6. // @description 一站式体验,JavBus & JavDB 兼容
  7. // @include *
  8. // @icon https://s1.ax1x.com/2022/04/01/q5lzYn.png
  9. // @resource success https://s1.ax1x.com/2022/04/01/q5l2LD.png
  10. // @resource info https://s1.ax1x.com/2022/04/01/q5lyz6.png
  11. // @resource warn https://s1.ax1x.com/2022/04/01/q5lgsO.png
  12. // @resource error https://s1.ax1x.com/2022/04/01/q5lcQK.png
  13. // @supportURL https://t.me/+bAWrOoIqs3xmMjll
  14. // @connect *
  15. // @run-at document-start
  16. // @grant unsafeWindow
  17. // @grant GM_removeValueChangeListener
  18. // @grant GM_addValueChangeListener
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_getResourceURL
  21. // @grant GM_xmlhttpRequest
  22. // @grant GM_notification
  23. // @grant GM_setClipboard
  24. // @grant GM_deleteValue
  25. // @grant GM_addElement
  26. // @grant GM_listValues
  27. // @grant GM_openInTab
  28. // @grant GM_addStyle
  29. // @grant GM_getValue
  30. // @grant GM_setValue
  31. // @grant GM_info
  32. // @license GPL-3.0-only
  33. // @compatible chrome last 2 versions
  34. // @compatible edge last 2 versions
  35. // ==/UserScript==
  36.  
  37. // prettier-ignore
  38. (function () {
  39. const NUM_REGEX = /\d+\.?\d*/g;
  40. const ZH_REGEX = /中文|字幕|中字|(-|_)c(?!d)/i;
  41. const CODE_REGEX = /^[a-z0-9]+(-|\w)+/i;
  42. const VR_REGEX = /(?<![a-z])VR(?![a-z-])/i;
  43. const FC2_REGEX = /^FC2-/i;
  44.  
  45. const REP = "%s";
  46. const DOC = document;
  47. const VOID = "javascript:void(0)";
  48. const PICK_RUL = "https://v.anxia.com/?pickcode=";
  49. const FILE_RUL = `https://115.com/?cid=${REP}&offset=0&mode=wangpan`;
  50. const TAB_NAME = { img: "大图", video: "预览", player: "视频" };
  51.  
  52. const addListener = EventTarget.prototype.addEventListener;
  53. EventTarget.prototype.addEventListener = function (type, ...args) {
  54. if (type !== "XContentLoaded") return addListener.call(this, type, ...args);
  55. document.readyState !== "loading" ? args[0]() : addListener.call(this, "DOMContentLoaded", ...args);
  56. };
  57.  
  58. // capture
  59. const captureJump = () => {
  60. const { hash } = location;
  61. if (!hash?.includes("#jump#")) return;
  62.  
  63. const code = hash.split("#").at(-1);
  64. if (!CODE_REGEX.test(code)) return;
  65.  
  66. DOC.addEventListener(
  67. "XContentLoaded",
  68. () => {
  69. const capture = captureQuery(code)?.href;
  70. if (capture) location.replace(capture);
  71. },
  72. { once: true }
  73. );
  74. };
  75. const captureQuery = (code, { body } = DOC) => {
  76. const nodeList = Array.from(body.querySelectorAll(":any-link"));
  77. const { length } = nodeList;
  78. if (!length) return;
  79.  
  80. const { regex } = codeParse(code);
  81. code = code.toUpperCase();
  82.  
  83. const res = [];
  84. for (let i = 0; i < length; i++) {
  85. const { href = "", textContent = "", innerHTML = "" } = nodeList[i];
  86.  
  87. if (!href || /^#|javascript/i.test(href)) continue;
  88. const str = [textContent, innerHTML]
  89. .map(item => item?.trim() ?? "")
  90. .filter(Boolean)
  91. .join("&")
  92. .toUpperCase();
  93. if (!str || !regex.test(str)) continue;
  94.  
  95. if (!str.includes(code)) {
  96. res.push({ href, zh: false });
  97. continue;
  98. }
  99.  
  100. if (ZH_REGEX.test(textContent)) return { href, zh: true };
  101. res.unshift({ href, zh: false });
  102.  
  103. if (nodeList.some(item => item.href === href && ZH_REGEX.test(item.textContent))) return { href, zh: true };
  104. }
  105. if (res.length) return res[0];
  106. };
  107. const codeParse = code => {
  108. const _ = FC2_REGEX.test(code) ? "|_" : "";
  109. code = code.split("-").map((item, index) => (index ? item.replace(/^0/, "") : item));
  110.  
  111. return {
  112. prefix: code[0],
  113. regex: new RegExp(`(?<![a-z])${code.join(`(0|-${_}){0,4}`)}(?!\\d)`, "i"),
  114. };
  115. };
  116. captureJump();
  117.  
  118. // match
  119. const MatchDomains = [
  120. { domain: "JavDB", regex: /javdb\d*\.com/ },
  121. { domain: "JavBus", regex: /(jav|bus|dmm|see|cdn|fan){2}\.[a-z]{2,}/ },
  122. { domain: "Drive115", regex: /captchaapi\.115\.com/ },
  123. ];
  124. const Domain = MatchDomains.find(item => item.regex.test(location.host))?.domain;
  125. if (!Domain) return;
  126.  
  127. // request
  128. const request = (url, data, method = "GET", options = {}) => {
  129. method = method ? method.toUpperCase().trim() : "GET";
  130. if (!url || !["GET", "HEAD", "POST"].includes(method)) return;
  131.  
  132. if (Object.prototype.toString.call(data) === "[object Object]") {
  133. data = Object.keys(data)
  134. .map(key => `${key}=${encodeURIComponent(data[key])}`)
  135. .join("&");
  136. }
  137. const { responseType, headers = {} } = options;
  138. if (method === "GET") {
  139. options.responseType = responseType ?? "document";
  140. if (data) {
  141. let joiner = "?";
  142. if (url.includes(joiner)) joiner = /\?|&$/.test(url) ? "" : "&";
  143. url = `${url}${joiner}${data}`;
  144. }
  145. }
  146. if (method === "POST") {
  147. options.responseType = responseType ?? "json";
  148. options.headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", ...headers };
  149. }
  150. return new Promise(resolve => {
  151. GM_xmlhttpRequest({
  152. url,
  153. data,
  154. method,
  155. timeout: 30000,
  156. onload: ({ status, response }) => {
  157. if (status >= 400) response = false;
  158. if (response && ["", "text"].includes(options.responseType)) {
  159. if (/<\/?[a-z][\s\S]*>/i.test(response)) {
  160. response = new DOMParser().parseFromString(response, "text/html");
  161. } else if (/^\{.*\}$/.test(response)) {
  162. response = JSON.parse(response);
  163. }
  164. }
  165. resolve(response ?? true);
  166. },
  167. ontimeout: () => resolve(false),
  168. onerror: () => resolve(false),
  169. ...options,
  170. });
  171. });
  172. };
  173. // utils
  174. const notify = ({ text = "点击跳转115", type = "error", clickUrl = "https://115.com/", setTab, ...details }) => {
  175. GM_notification({
  176. text: text || GM_info.script.name,
  177. image: GM_getResourceURL(type),
  178. highlight: false,
  179. silent: true,
  180. timeout: 5000,
  181. onclick: () => openInTab(clickUrl),
  182. ...details,
  183. });
  184. if (setTab) setTabBar({ type, title: details.title });
  185. };
  186. const addPrefetch = arr => {
  187. arr = arr.filter(Boolean);
  188. if (!arr.length) return;
  189.  
  190. DOC.head.insertAdjacentHTML("beforeend", arr.map(item => `<link rel="prefetch" href="${item}">`).join(""));
  191. };
  192. const setTabBar = ({ type = GM_info.script.icon, title }) => {
  193. if (title) DOC.title = title;
  194. type = type.includes("http") ? type : GM_getResourceURL(type);
  195.  
  196. let icons = DOC.querySelectorAll("link[rel*='icon']");
  197. if (!icons?.length) icons = [DOC.create("link", { type: "image/x-icon", rel: "shortcut icon" })];
  198.  
  199. icons.forEach(node => {
  200. node.href = type;
  201. DOC.getElementsByTagName("head")[0].append(node);
  202. });
  203. };
  204. const openInTab = (url, active = true, options = {}) => {
  205. if (url) return GM_openInTab(url, { active: !!active, setParent: true, ...options });
  206. };
  207. const imgLoaded = img => img.classList.add("x-in");
  208. const fadeInImg = node => {
  209. const img = node.querySelector("img");
  210. if (!img) return;
  211.  
  212. img.title = "";
  213. if (img.complete && img.naturalHeight !== 0) return imgLoaded(img);
  214. img.onload = () => imgLoaded(img);
  215. };
  216. const addCopy = (selectors, attrs = {}) => {
  217. const node = typeof selectors === "string" ? DOC.querySelector(selectors) : selectors;
  218. const _attrs = { "data-copy": node.textContent, class: "x-ml", href: VOID };
  219. const target = DOC.create("a", { ..._attrs, ...attrs }, "复制");
  220. target.addEventListener("click", e => handleCopy(e, "成功"));
  221. node.append(target);
  222. };
  223. const handleCopy = (e, tip = "复制成功", copy) => {
  224. const { target } = e;
  225. copy = copy ? copy : target?.dataset?.copy?.trim() ?? "";
  226. if (!copy) return;
  227.  
  228. e.preventDefault();
  229. e.stopPropagation();
  230. GM_setClipboard(copy);
  231.  
  232. const { textContent = "" } = target;
  233. target.textContent = tip || "复制成功";
  234. const timer = setTimeout(() => {
  235. clearTimeout(timer);
  236. target.textContent = textContent;
  237. }, 300);
  238. };
  239. const getDate = (timestamp, joiner = "-") => {
  240. const date = timestamp ? new Date(timestamp) : new Date();
  241. const Y = date.getFullYear();
  242. const M = `${date.getMonth() + 1}`.padStart(2, "0");
  243. const D = `${date.getDate()}`.padStart(2, "0");
  244. return `${Y}${joiner}${M}${joiner}${D}`;
  245. };
  246. const transToBytes = (sizeStr = "") => {
  247. const sizeNum = sizeStr?.match(NUM_REGEX)?.[0] ?? 0;
  248. if (sizeNum <= 0) return 0;
  249.  
  250. const matchList = [
  251. { unit: /byte/gi, transform: size => size },
  252. { unit: /kb/gi, transform: size => size * 1000 },
  253. { unit: /mb/gi, transform: size => size * 1000 ** 2 },
  254. { unit: /gb/gi, transform: size => size * 1000 ** 3 },
  255. { unit: /kib/gi, transform: size => size * 1024 },
  256. { unit: /mib/gi, transform: size => size * 1024 ** 2 },
  257. { unit: /gib/gi, transform: size => size * 1024 ** 3 },
  258. ];
  259.  
  260. return (
  261. matchList
  262. .find(({ unit }) => unit.test(sizeStr))
  263. ?.transform(sizeNum)
  264. ?.toFixed(2) ?? 0
  265. );
  266. };
  267. DOC.create = (tag, attrs = {}, child) => {
  268. const node = DOC.createElement(tag);
  269. for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
  270. if (child) node.append(child);
  271. return node;
  272. };
  273. const createVideo = (sources, attrs = {}) => {
  274. if (typeof sources === "string") sources = [{ src: sources, type: "video/mp4" }];
  275. const video = DOC.create("video", attrs);
  276. video.controls = true;
  277. video.currentTime = 3;
  278. video.volume = localStorage.getItem("volume") ?? 0;
  279. video.preload = "none";
  280. video.append(...sources.map(item => DOC.create("source", item)));
  281.  
  282. let timer = null;
  283. video.addEventListener("volumechange", ({ target }) => {
  284. if (timer) clearTimeout(timer);
  285. timer = setTimeout(() => localStorage.setItem("volume", target.volume), 1000);
  286. });
  287. video.addEventListener("keyup", ({ code, target }) => {
  288. if (code === "Enter") {
  289. target.requestFullscreen();
  290. target.play();
  291. }
  292. if (code === "KeyM") target.muted = !target.muted;
  293. });
  294. return video;
  295. };
  296. const paramParse = (param, filter, separator = "#") => {
  297. return unique(param.split(separator).filter(item => filter.includes(item)));
  298. };
  299. const unique = (arr, key) => {
  300. if (!key) return Array.from(new Set(arr));
  301. return unique(arr.map(e => e[key].toLowerCase())).map(e => arr.find(x => x[key].toLowerCase() === e));
  302. };
  303. const verify = () => {
  304. const h = 667;
  305. const w = 375;
  306. const t = (window.screen.availHeight - h) / 2;
  307. const l = (window.screen.availWidth - w) / 2;
  308.  
  309. window.open(
  310. `https://captchaapi.115.com/?ac=security_code&type=web&cb=Close911_${new Date().getTime()}`,
  311. "验证账号",
  312. `height=${h},width=${w},top=${t},left=${l},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
  313. );
  314. };
  315. const delay = (s = 1) => {
  316. return new Promise(r => {
  317. setTimeout(r, s * 1000);
  318. });
  319. };
  320.  
  321. class Store {
  322. static init() {
  323. const date = getDate();
  324. const cdKey = "CD";
  325.  
  326. if (GM_getValue(cdKey, "") !== date) {
  327. GM_setValue(cdKey, date);
  328. GM_setValue("DETAILS", {});
  329. GM_setValue("RESOURCE", {});
  330. GM_setValue("VERIFY_STATUS", "");
  331. }
  332.  
  333. const expired = getDate(new Date() - 1000 * 60 * 60 * 24 * 30);
  334. if ((localStorage.getItem(cdKey) ?? expired) > expired) return;
  335. localStorage.clear();
  336. localStorage.setItem(cdKey, date);
  337. }
  338. static getDetail(key) {
  339. const localDetail = {
  340. img: localStorage.getItem(`${key}_img`) ?? undefined,
  341. video: localStorage.getItem(`${key}_video`) ?? undefined,
  342. };
  343.  
  344. const details = GM_getValue("DETAILS", {});
  345. return { ...localDetail, ...(details[key] ?? {}) };
  346. }
  347. static upDetail(key, val) {
  348. const details = GM_getValue("DETAILS", {});
  349. details[key] = { ...(details[key] ?? {}), ...val };
  350. GM_setValue("DETAILS", details);
  351.  
  352. if (FC2_REGEX.test(key)) return;
  353. val = JSON.stringify(val, ["img", "video"]);
  354. if (val === "{}") return;
  355. for (const [_key, _val] of Object.entries(JSON.parse(val))) {
  356. localStorage.setItem(`${key}_${_key}`, _val);
  357. }
  358. }
  359. static getResource(key) {
  360. const resource = GM_getValue("RESOURCE", {});
  361. return resource[key];
  362. }
  363. static upResource(key, val) {
  364. const resource = GM_getValue("RESOURCE", {});
  365. resource[key] = val;
  366. GM_setValue("RESOURCE", resource);
  367. }
  368. static getVerifyStatus() {
  369. return GM_getValue("VERIFY_STATUS", "");
  370. }
  371. static setVerifyStatus(val) {
  372. GM_setValue("VERIFY_STATUS", val);
  373. }
  374. }
  375. class Apis {
  376. static async fetchBlogJav(code) {
  377. if (FC2_REGEX.test(code)) code = code.replace("-PPV-", " PPV ");
  378. const re = { img: "" };
  379.  
  380. const requests = [
  381. {
  382. url: `https://blogjav.net/?s=${code}`,
  383. selectors: "#main .entry-title a",
  384. },
  385. {
  386. url: `https://duckduckgo.com/?q=${code} site:blogjav.net`,
  387. selectors: "#links h2 a",
  388. },
  389. {
  390. url: `https://www.google.com/search?q=${code} site:blogjav.net`,
  391. selectors: "#rso .g .yuRUbf > a",
  392. },
  393. ];
  394. let list = await Promise.allSettled(requests.map(item => request(item.url)));
  395.  
  396. const { regex } = codeParse(code);
  397. code = code.toUpperCase();
  398. let url = "";
  399. for (let index = 0, { length } = requests; index < length; index++) {
  400. const { status, value } = list[index];
  401. if (status !== "fulfilled" || !value) continue;
  402.  
  403. const href = Array.from(value?.querySelectorAll(requests[index].selectors) ?? []).find(item => {
  404. const str = item.textContent.toUpperCase();
  405. return regex.test(str) && str.includes(code);
  406. })?.href;
  407. if (!href) continue;
  408.  
  409. url = href;
  410. break;
  411. }
  412. if (!url) return re;
  413.  
  414. list = await Promise.allSettled([
  415. request(url),
  416. request(`http://webcache.googleusercontent.com/search?q=cache:${url}`),
  417. ]);
  418. url = "";
  419. for (let index = 0, { length } = list; index < length; index++) {
  420. const { status, value } = list[index];
  421. if (status !== "fulfilled" || !value) continue;
  422.  
  423. let img = value.querySelector("#main .entry-content a img");
  424. if (!img) continue;
  425.  
  426. img = img.getAttribute("data-src") ?? img.getAttribute("data-lazy-src");
  427. if (!img) continue;
  428.  
  429. url = img.replace("//t", "//img").replace("thumbs", "images");
  430. break;
  431. }
  432. re.img = url;
  433. return re;
  434. }
  435. static async fetchJavStore(code) {
  436. if (FC2_REGEX.test(code)) code = code.replace("-PPV-", "PPV ");
  437. const re = { img: "" };
  438.  
  439. let res = await request(`https://javstore.net/search/${code}.html`);
  440. if (!res) return re;
  441.  
  442. res = res.querySelectorAll("#content_news li > a");
  443. if (!res.length) return re;
  444.  
  445. const { regex } = codeParse(code);
  446. code = code.toUpperCase();
  447. res = Array.from(res).find(item => {
  448. const str = item.title.toUpperCase();
  449. return str.includes(code) && regex.test(str) && item.querySelector("img").src.startsWith("https");
  450. })?.href;
  451. if (!res) return re;
  452.  
  453. res = await request(res);
  454. if (!res) return re;
  455.  
  456. const img = res.querySelector(".news > a")?.href;
  457. if (img && /\.(jpg|png)$/i.test(img)) re.img = img;
  458. return re;
  459. }
  460. static async fetchVideoByGuess({ code, isVR, cid }) {
  461. const re = { video: "" };
  462. if (!code && !cid) return re;
  463.  
  464. const HOST = `https://cc3001.dmm.co.jp/${isVR ? "vrsample" : "litevideo/freepv"}/`;
  465. const SUFFIX = isVR ? "vrlite" : "_dmb_w";
  466. const list = [];
  467.  
  468. const parseUrl = (prefix, num = "") => {
  469. return `${HOST}${prefix[0]}/${prefix.substring(0, 3)}/${prefix}${num}/${prefix}${num}${SUFFIX}.mp4`;
  470. };
  471.  
  472. if (code) {
  473. const [prefix, num = ""] = code.toLowerCase().split(/-|_/);
  474. const matchList = [
  475. { prefix: ["fera", "fuga", "jrze", "mesu"], add: "h_086" },
  476. { prefix: ["spz", "udak", "ofku"], add: "h_254" },
  477. { prefix: ["abp", "ppt", "abw"], add: "118" },
  478. { prefix: ["mds", "scpx"], add: "84" },
  479. { prefix: ["shind"], add: "h_1560" },
  480. { prefix: ["pydvr"], add: "h_1321" },
  481. { prefix: ["meko"], add: "h_1160" },
  482. { prefix: ["fgan"], add: "h_1440" },
  483. { prefix: ["spro"], add: "h_1594" },
  484. { prefix: ["hzgb"], add: "h_1100" },
  485. { prefix: ["stcv"], add: "h_1616" },
  486. { prefix: ["skmj"], add: "h_1324" },
  487. { prefix: ["haru"], add: "h_687" },
  488. { prefix: ["fone"], add: "h_491" },
  489. { prefix: ["fsvr"], add: "h_955" },
  490. { prefix: ["jukf"], add: "h_227" },
  491. { prefix: ["rebd"], add: "h_346" },
  492. { prefix: ["ktra"], add: "h_094" },
  493. { prefix: ["supa"], add: "h_244" },
  494. { prefix: ["zex"], add: "h_720" },
  495. { prefix: ["pym"], add: "h_283" },
  496. { prefix: ["hz"], add: "h_113" },
  497. { prefix: ["dtvr"], add: "24" },
  498. { prefix: ["umd"], add: "125" },
  499. { prefix: ["gvg"], add: "13" },
  500. { prefix: ["t28"], add: "55" },
  501. { prefix: ["lol"], add: "12" },
  502. { prefix: ["dv"], add: "53" },
  503. ];
  504. const add = matchList.find(item => item.prefix.includes(prefix))?.add ?? "1";
  505. list.push(parseUrl(prefix, num));
  506. list.push(parseUrl(`${add}${prefix}`, num));
  507. if (num) {
  508. list.push(parseUrl(prefix, `00${num}`));
  509. list.push(parseUrl(`${add}${prefix}`, `00${num}`));
  510. }
  511. }
  512. if (cid) {
  513. list.push(parseUrl(cid));
  514. list.push(parseUrl(cid.replace("00", "")));
  515. }
  516.  
  517. const res = await Promise.allSettled(unique(list).map(item => request(item, {}, "HEAD")));
  518. for (let index = 0, { length } = res; index < length; index++) {
  519. const { status, value } = res[index];
  520. if (status !== "fulfilled" || !value) continue;
  521.  
  522. re.video = list[index];
  523. break;
  524. }
  525. return re;
  526. }
  527. static async fetchVideoByStudio({ code, studio }) {
  528. const re = { video: "" };
  529. if (!studio) return re;
  530.  
  531. code = code.toLowerCase();
  532. const matchList = [
  533. {
  534. name: "東京熱",
  535. match: `https://my.cdn.tokyo-hot.com/media/samples/${REP}.mp4`,
  536. },
  537. {
  538. name: "カリビアンコム",
  539. match: `https://smovie.caribbeancom.com/sample/movies/${REP}/720p.mp4`,
  540. },
  541. {
  542. name: "一本道",
  543. match: `http://smovie.1pondo.tv/sample/movies/${REP}/1080p.mp4`,
  544. },
  545. {
  546. name: "HEYZO",
  547. trans: code => code.replace(/HEYZO-/gi, ""),
  548. match: `https://sample.heyzo.com/contents/3000/${REP}/heyzo_hd_${REP}_sample.mp4`,
  549. },
  550. {
  551. name: "天然むすめ",
  552. match: `https://smovie.10musume.com/sample/movies/${REP}/720p.mp4`,
  553. },
  554. {
  555. name: "パコパコママ",
  556. match: `https://fms.pacopacomama.com/hls/sample/pacopacomama.com/${REP}/720p.mp4`,
  557. },
  558. ];
  559. let res = matchList.find(({ name }) => name === studio);
  560. if (!res) return re;
  561.  
  562. res = res.match.replaceAll(REP, res.trans ? res.trans(code) : code);
  563. if (await request(res, {}, "HEAD")) re.video = res;
  564. return re;
  565. }
  566. // fetch video by guess for list
  567. static async fetchVBGFL(code) {
  568. const re = { video: "" };
  569.  
  570. code = code.replace(/HEYZO-/gi, "").toLowerCase();
  571. const list = [
  572. `https://my.cdn.tokyo-hot.com/media/samples/${code}.mp4`,
  573. `https://smovie.caribbeancom.com/sample/movies/${code}/720p.mp4`,
  574. `http://smovie.1pondo.tv/sample/movies/${code}/1080p.mp4`,
  575. `https://sample.heyzo.com/contents/3000/${code}/heyzo_hd_${code}_sample.mp4`,
  576. `https://smovie.10musume.com/sample/movies/${code}/720p.mp4`,
  577. `https://fms.pacopacomama.com/hls/sample/pacopacomama.com/${code}/720p.mp4`,
  578. ];
  579.  
  580. const res = await Promise.allSettled(list.map(item => request(item, {}, "HEAD")));
  581. for (let index = 0, { length } = res; index < length; index++) {
  582. const { status, value } = res[index];
  583. if (status !== "fulfilled" || !value) continue;
  584.  
  585. re.video = list[index];
  586. break;
  587. }
  588. return re;
  589. }
  590. // fetch video by local
  591. static async fetchVBL({ node, isVR }) {
  592. const re = { video: "" };
  593.  
  594. let res = await request(node.href);
  595. if (!res) return re;
  596.  
  597. if (Domain === "JavBus") {
  598. let cid = res.querySelector("#sample-waterfall a.sample-box")?.href;
  599. cid = cid?.includes("pics.dmm.co.jp") ? cid.split("/").at(-2) : "";
  600. if (!cid) return re;
  601.  
  602. res = await this.fetchVideoByGuess({ cid, isVR });
  603. if (res.video) re.video = res.video;
  604. }
  605. if (Domain === "JavDB") {
  606. res = res.querySelector("#preview-video source")?.getAttribute("src") ?? "";
  607. if (res && (await request(res, {}, "HEAD"))) re.video = res;
  608. }
  609.  
  610. return re;
  611. }
  612. static async fetchJavSpyl(code) {
  613. const re = { video: "" };
  614.  
  615. let res = await request("https://v2.javspyl.tk/api/", { ID: code }, "POST", {
  616. headers: { origin: "https://javspyl.tk" },
  617. responseType: "json",
  618. });
  619. if (!res) return re;
  620.  
  621. res = res?.info?.url;
  622. if (!res || /\.m3u8?$/i.test(res)) return re;
  623.  
  624. res = res.includes("//") ? res : `https://${res}`;
  625. if (await request(res, {}, "HEAD")) re.video = res;
  626. return re;
  627. }
  628. static async fetchAVPreview(code) {
  629. const re = { video: "" };
  630.  
  631. let res = await request(`https://avpreview.com/zh/search?keywords=${code}`);
  632. if (!res) return re;
  633.  
  634. res = Array.from(res.querySelectorAll(".container .videobox"))
  635. .find(item => code === item.querySelector("h2 strong")?.textContent)
  636. ?.querySelector("a")
  637. ?.getAttribute("href");
  638. if (!res) return re;
  639.  
  640. res = await request(
  641. "https://avpreview.com/API/v1.0/index.php",
  642. {
  643. system: "videos",
  644. action: "detail",
  645. contentid: res.split("/").at(-1),
  646. sitecode: "avpreview",
  647. ip: "",
  648. token: "",
  649. },
  650. "GET",
  651. { responseType: "json" }
  652. );
  653. if (!res) return re;
  654.  
  655. res = res?.videos?.trailer?.replace("/hlsvideo/", "/litevideo/")?.replace("/playlist.m3u8", "");
  656. if (!res) return re;
  657.  
  658. const contentId = res.split("/").at(-1);
  659. res = [
  660. `${res}/${contentId}_dmb_w.mp4`,
  661. `${res}/${contentId}_mhb_w.mp4`,
  662. `${res}/${contentId}_dm_w.mp4`,
  663. `${res}/${contentId}_sm_w.mp4`,
  664. ];
  665. const list = await Promise.allSettled(res.map(item => request(item, {}, "HEAD")));
  666. for (let index = 0, { length } = list; index < length; index++) {
  667. const { status, value } = list[index];
  668. if (status !== "fulfilled" || !value) continue;
  669.  
  670. re.video = res[index];
  671. break;
  672. }
  673. return re;
  674. }
  675. static async fetchFC2(code) {
  676. code = code.split("-").at(-1);
  677.  
  678. const HOST = "https://adult.xiaojiadianmovie.be/";
  679. const re = { video: "" };
  680.  
  681. let res = await request(`${HOST}article/${code}/`);
  682. if (!res) return re;
  683.  
  684. res = res.head
  685. .querySelector("script:last-child")
  686. .innerHTML?.match(/(?<=ae', ').*?(?=')/)
  687. ?.at(0);
  688. if (!res) return re;
  689.  
  690. res = await request(`${HOST}api/v2/videos/${code}/sample`, {}, "GET", {
  691. responseType: "json",
  692. headers: { "X-FC2-Contents-Access-Token": res },
  693. });
  694. if (res?.path && (await request(res.path, {}, "HEAD"))) re.video = res.path;
  695. return re;
  696. }
  697. static async fetchNetflav(code) {
  698. const HOST = "https://netflav.com";
  699. const re = { player: [] };
  700.  
  701. let res = await request(`${HOST}/search?type=title&keyword=${code}`);
  702. if (!res) return re;
  703.  
  704. res = res.querySelectorAll(".grid_root .grid_cell");
  705. if (!res.length) return re;
  706.  
  707. const { regex } = codeParse(code);
  708. code = code.toUpperCase();
  709. res = Array.from(res)
  710. .find(item => {
  711. const str = item.querySelector(".grid_title").textContent.toUpperCase();
  712. return str.includes(code) && regex.test(str);
  713. })
  714. ?.querySelector("a")
  715. ?.getAttribute("href");
  716. if (!res) return re;
  717.  
  718. res = await request(`${HOST}${res}`);
  719. if (!res) return re;
  720.  
  721. res = res.querySelector("#__NEXT_DATA__")?.textContent;
  722. if (!res) return re;
  723.  
  724. res = JSON.parse(res)?.props?.initialState?.video?.data?.srcs;
  725. if (!res?.length) return re;
  726.  
  727. const matchList = [
  728. {
  729. regex: /\/\/(mm9842\.com|www\.avple\.video|asianclub\.tv)/,
  730. parse: async url => {
  731. const [protocol, href] = url.split("//");
  732. const [host, ...pathname] = href.split("/");
  733.  
  734. const res = await request(
  735. `${protocol}//${host}/api/source/${pathname.pop()}`,
  736. { r: "", d: host },
  737. "POST"
  738. );
  739.  
  740. if (!res?.success) return [];
  741. return (res?.data ?? []).map(({ file, label = "320p", type }) => {
  742. return { src: file, title: label, type: `video/${type}` };
  743. });
  744. },
  745. },
  746. {
  747. regex: /\/\/(embedgram\.com|vidoza\.net)/,
  748. parse: async url => {
  749. const res = await request(url);
  750. if (!res) return [];
  751. return Array.from(res?.querySelectorAll("video source") ?? []).map(
  752. ({ src, title = "320p", type }) => {
  753. return { src, title, type };
  754. }
  755. );
  756. },
  757. },
  758. ];
  759. res = await Promise.allSettled(res.map(url => matchList.find(({ regex }) => regex.test(url))?.parse(url)));
  760. for (let index = 0, { length } = res; index < length; index++) {
  761. const { status, value } = res[index];
  762. if (status === "fulfilled" && value?.length) re.player = re.player.concat(value);
  763. }
  764. return re;
  765. }
  766. static async fetchTranslate(str) {
  767. const re = { title: "" };
  768.  
  769. const res = await request(
  770. "https://www.google.com/async/translate?vet=12ahUKEwixq63V3Kn3AhUCJUQIHdMJDpkQqDh6BAgCECw..i&ei=CbNjYvGCPYLKkPIP05O4yAk&yv=3",
  771. {
  772. async: `translate,sl:auto,tl:zh-CN,st:${encodeURIComponent(
  773. str.trim()
  774. )},id:1650701080679,qc:true,ac:false,_id:tw-async-translate,_pms:s,_fmt:pc`,
  775. },
  776. "POST",
  777. { responseType: "" }
  778. );
  779. if (res) re.title = res?.querySelector("#tw-answ-target-text")?.textContent ?? "";
  780.  
  781. return re;
  782. }
  783. static async _fetchMagnet(code, { site, host, search, selectors, filter }) {
  784. const key = `${site.substring(0, 3).toLowerCase()}_magnet`;
  785. const re = { [key]: [] };
  786.  
  787. let res = await request(`${host}${search}`);
  788. if (!res) return re;
  789.  
  790. res = res.querySelectorAll(selectors);
  791. if (!res.length) return re;
  792.  
  793. const { regex } = codeParse(code);
  794. for (const item of res) {
  795. const name = (filter.name(item) ?? "").replace(/[\u200B-\u200D\uFEFF]/g, "");
  796. if (!regex.test(name)) continue;
  797.  
  798. const size = filter.size(item);
  799. let href = filter.href(item);
  800. if (href && !href.includes("//")) href = `${host}${href.replace(/^\//, "")}`;
  801.  
  802. re[key].push({
  803. name,
  804. zh: ZH_REGEX.test(name),
  805. link: filter.link(item).split("&")[0],
  806. size,
  807. bytes: transToBytes(size),
  808. date: filter.date(item),
  809. href,
  810. from: site,
  811. });
  812. }
  813. return re;
  814. }
  815. static fetchSukebei(code) {
  816. return this._fetchMagnet(code, {
  817. site: "Sukebei",
  818. host: "https://sukebei.nyaa.si/",
  819. search: `?f=0&c=0_0&q=${code}`,
  820. selectors: ".table-responsive table tbody tr",
  821. filter: {
  822. name: e => e.querySelector("td:nth-child(2) a").textContent,
  823. link: e => e.querySelector("td:nth-child(3) a:last-child").href,
  824. size: e => e.querySelector("td:nth-child(4)").textContent,
  825. date: e => e.querySelector("td:nth-child(5)").textContent.split(" ")[0],
  826. href: e => e.querySelector("td:nth-child(2) a").getAttribute("href"),
  827. },
  828. });
  829. }
  830. static fetchBTSOW(code) {
  831. return this._fetchMagnet(code, {
  832. site: "BTSOW",
  833. host: "https://btsow.com/",
  834. search: `search/${code}`,
  835. selectors: ".data-list .row:not(.hidden-xs)",
  836. filter: {
  837. name: e => e.querySelector(".file").textContent,
  838. link: e => `magnet:?xt=urn:btih:${e.querySelector("a").href.split("/").pop()}`,
  839. size: e => e.querySelector(".size").textContent,
  840. date: e => e.querySelector(".date").textContent,
  841. href: e => e.querySelector("a").getAttribute("href"),
  842. },
  843. });
  844. }
  845. static fetchBTDigg(code, index = 0) {
  846. return this._fetchMagnet(code, {
  847. site: "BTDigg",
  848. host: `https://${index ? "www." : ""}btdig.com/`,
  849. search: `search?order=0&q=${code}`,
  850. selectors: ".one_result",
  851. filter: {
  852. name: e => e.querySelector(".torrent_name").textContent,
  853. link: e => e.querySelector(".torrent_magnet a").href,
  854. size: e => e.querySelector(".torrent_size").textContent,
  855. date: e => e.querySelector(".torrent_age").textContent,
  856. href: e => e.querySelector(".torrent_name a").href,
  857. },
  858. });
  859. }
  860. static async fetchMGS({ code, title }) {
  861. const HOST = "https://www.mgstage.com";
  862. const re = { mgs_score: [], video: "" };
  863.  
  864. let res = await request(`${HOST}/search/cSearch.php?search_word=${title}&list_cnt=120`);
  865. if (!res) return re;
  866.  
  867. res = res.querySelectorAll(".rank_list li");
  868. if (!res.length) return re;
  869.  
  870. const { regex } = codeParse(code);
  871. code = code.toUpperCase();
  872. res = Array.from(res).find(item => {
  873. const str = item.querySelector("a").getAttribute("href").toUpperCase();
  874. return str.includes(code) && regex.test(str);
  875. });
  876. if (!res) return re;
  877.  
  878. let [score, video] = await Promise.allSettled([
  879. request(`${HOST}${res.querySelector("a").getAttribute("href")}`),
  880. request(
  881. `${HOST}${res
  882. .querySelector(".sample_movie_btn a")
  883. .getAttribute("href")
  884. .replace("player.html/", "Respons.php?pid=")}`,
  885. {},
  886. "GET",
  887. { responseType: "json" }
  888. ),
  889. ]);
  890. if (score.status === "fulfilled" && score.value) {
  891. score = score.value.querySelector(".detail_data .review")?.textContent?.match(NUM_REGEX);
  892. if (score?.length) re.mgs_score.push({ score: score[0], total: 5, num: score[1], from: "MGS" });
  893. }
  894. if (video.status === "fulfilled" && video.value) {
  895. video = video.value?.url.split("?")[0].replace("ism/request", "mp4");
  896. if (await request(video, {}, "HEAD")) re.video = video;
  897. }
  898. return re;
  899. }
  900. static async fetchLib(code) {
  901. const HOST = "https://www.javlibrary.com/cn/";
  902. const re = { lib_score: [], star: [] };
  903.  
  904. let res = await request(`${HOST}vl_searchbyid.php?keyword=${code}`);
  905. if (!res) return re;
  906.  
  907. if (res.querySelector(".videothumblist")) {
  908. res = res.querySelectorAll(".videothumblist .videos .video > a");
  909. if (!res.length) return re;
  910.  
  911. const { regex } = codeParse(code);
  912. code = code.toUpperCase();
  913. res = Array.from(res).find(({ title }) => {
  914. const str = title.toUpperCase();
  915. return str.includes(code) && regex.test(str);
  916. });
  917. if (!res) return re;
  918.  
  919. res = await request(`${HOST}${res.getAttribute("href")}`);
  920. }
  921.  
  922. const score = res.querySelector("#video_review .text .score")?.textContent?.replace(/\(|\)/g, "") ?? "";
  923. if (score) re.lib_score.push({ score, total: 10, from: "JavLibrary" });
  924.  
  925. re.star = Array.from(res.querySelectorAll("#video_cast .text .star")).map(item => item.textContent);
  926. return re;
  927. }
  928. static async fetchDMM({ code, title }) {
  929. const re = { fetchVideo: "", fetchInfo: "" };
  930.  
  931. let res = await request(`https://jav.land/tw/id_search.php?keys=${code.toUpperCase()}`);
  932. if (!res) return re;
  933.  
  934. res = res.querySelector("table.videotextlist tbody tr");
  935. if (!res) return re;
  936.  
  937. res = res.querySelectorAll("td");
  938. if (!res?.length || res[0].textContent !== "內容 ID:") return re;
  939. const cid = res[1].textContent;
  940.  
  941. re.fetchVideo = async () => {
  942. const result = { video: "" };
  943.  
  944. const first = cid[0];
  945. const second = cid.substring(0, 3);
  946. const list = [
  947. `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_dmb_w.mp4`,
  948. `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_mhb_w.mp4`,
  949. `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_dm_w.mp4`,
  950. `https://cc3001.dmm.co.jp/litevideo/freepv/${first}/${second}/${cid}/${cid}_sm_w.mp4`,
  951. `https://cc3001.dmm.co.jp/vrsample/${first}/${second}/${cid}/${cid}vrlite.mp4`,
  952. ];
  953.  
  954. const res = await Promise.allSettled(list.map(item => request(item, {}, "HEAD")));
  955. for (let index = 0, { length } = res; index < length; index++) {
  956. const { status, value } = res[index];
  957. if (status !== "fulfilled" || !value) continue;
  958.  
  959. result.video = list[index];
  960. break;
  961. }
  962. return result;
  963. };
  964.  
  965. re.fetchInfo = async () => {
  966. const result = { dmm_score: [], star: [], video: "" };
  967.  
  968. const res = await request(`https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=${cid}/`, {}, "GET", {
  969. headers: { "accept-language": "ja-JP" },
  970. });
  971. if (!res) return result;
  972.  
  973. let params = res.head.querySelector("script[type='application/ld+json']")?.textContent;
  974. if (!params) return result;
  975.  
  976. params = JSON.parse(params);
  977. if (!params?.name || (title && !title.includes(params.name))) return result;
  978.  
  979. if (params.aggregateRating) {
  980. result.dmm_score.push({
  981. score: params.aggregateRating.ratingValue,
  982. total: 5,
  983. num: params.aggregateRating.ratingCount,
  984. from: "DMM",
  985. });
  986. }
  987. result.star = Array.from(res.querySelectorAll("#performer a")).map(item => item.textContent);
  988. const video = params.subjectOf?.contentUrl;
  989. if (video && (await request(video, {}, "HEAD"))) result.video = video;
  990. return result;
  991. };
  992.  
  993. return re;
  994. }
  995. static async fetchDB(code) {
  996. const HOST = "https://javdb.com";
  997. const re = { db_score: [], star: [], video: "", sub: [] };
  998.  
  999. let res = await request(`${HOST}/search?q=${code}&sb=0`);
  1000. if (!res) return re;
  1001.  
  1002. res = res.querySelectorAll(".movie-list .item a");
  1003. if (!res.length) return re;
  1004.  
  1005. const { regex } = codeParse(code);
  1006. code = code.toUpperCase();
  1007. res = Array.from(res).find(item => {
  1008. const str = item.querySelector("strong").textContent.toUpperCase();
  1009. return str.includes(code) && regex.test(str);
  1010. });
  1011. if (!res) return re;
  1012.  
  1013. const href = `${HOST}${res.getAttribute("href")}`;
  1014. res = await request(href);
  1015. if (!res) return re;
  1016.  
  1017. const scoreReg = /評分|Rating/;
  1018. const starReg = /演員|Actor/;
  1019. for (const item of res.querySelectorAll(".movie-panel-info .panel-block")) {
  1020. let [label, value] = item.querySelectorAll("strong, .value");
  1021. if (!label || !value) continue;
  1022.  
  1023. label = label.textContent;
  1024. value = value.textContent;
  1025. if (scoreReg.test(label)) {
  1026. const [score, num] = value.match(NUM_REGEX);
  1027. re.db_score.push({ score, total: 5, num, from: "JavDB" });
  1028. continue;
  1029. }
  1030. if (!starReg.test(label)) continue;
  1031.  
  1032. re.star = value
  1033. .split("\n")
  1034. .filter(item => item.includes("♀"))
  1035. .map(item => item.replace("♀", "").trim());
  1036. }
  1037.  
  1038. if (res.querySelector("a.preview-video-container")) {
  1039. const video = res.querySelector("#preview-video source")?.src || "";
  1040. if (video && (await request(video, {}, "HEAD"))) re.video = video;
  1041. }
  1042.  
  1043. for (const item of res.querySelectorAll("#magnets-content .item")) {
  1044. const first = item.querySelector("a");
  1045. if (!first) continue;
  1046.  
  1047. const size = first.querySelector(".meta")?.textContent?.trim() ?? "";
  1048. re.sub.push({
  1049. from: "JavDB",
  1050. href,
  1051. name: first.querySelector(".name").textContent,
  1052. link: first.href.split("&")[0],
  1053. size,
  1054. bytes: transToBytes(size),
  1055. zh: !!first.querySelector(".tag.is-warning.is-small.is-light"),
  1056. date: item.querySelector(".time")?.textContent ?? "",
  1057. });
  1058. }
  1059. return re;
  1060. }
  1061. static async _driveSearch(params, keys = []) {
  1062. if (typeof params === "string") params = { search_value: params };
  1063.  
  1064. const res = await request(
  1065. "https://webapi.115.com/files/search",
  1066. {
  1067. offset: 0,
  1068. limit: 10000,
  1069. date: "",
  1070. aid: 1,
  1071. cid: 0,
  1072. pick_code: "",
  1073. type: 4,
  1074. source: "",
  1075. format: "json",
  1076. o: "user_ptime",
  1077. asc: 0,
  1078. star: "",
  1079. suffix: "",
  1080. ...params,
  1081. },
  1082. "GET",
  1083. { responseType: "json" }
  1084. );
  1085. if (!res) return;
  1086. if (!res.state) return notify({ title: res.error });
  1087.  
  1088. return res.data.map(({ n, pc, fid, cid, ...item }) => {
  1089. const _item = { n, pc, fid, cid };
  1090. for (const key of keys) _item[key] = item[key];
  1091. return _item;
  1092. });
  1093. }
  1094. static async _driveCid(name, type) {
  1095. let res = await this.driveFile();
  1096. if (!res) return;
  1097. if (!res.state) return notify({ title: res?.errNo === 20130827 ? "文件排序不支持" : res.error });
  1098.  
  1099. res = res.data.find(({ n }) => n === name)?.cid ?? "";
  1100. if (res || type === "find") return res;
  1101.  
  1102. res = await request("https://webapi.115.com/files/add", { pid: 0, cname: name }, "POST");
  1103. if (!res) return;
  1104. if (!res.state) return notify({ title: res.error });
  1105. return res?.cid ?? "";
  1106. }
  1107. static driveFile(params = {}) {
  1108. if (typeof params === "string") params = { cid: params };
  1109.  
  1110. return request(
  1111. "https://webapi.115.com/files",
  1112. {
  1113. aid: 1,
  1114. cid: 0,
  1115. o: "user_ptime",
  1116. asc: 0,
  1117. offset: 0,
  1118. show_dir: 1,
  1119. limit: 115,
  1120. code: "",
  1121. scid: "",
  1122. snap: 0,
  1123. natsort: 1,
  1124. record_open_time: 1,
  1125. source: "",
  1126. format: "json",
  1127. ...params,
  1128. },
  1129. "GET",
  1130. { responseType: "json" }
  1131. );
  1132. }
  1133. static async driveVideo(cid, keys = []) {
  1134. if (!cid) return;
  1135.  
  1136. const res = await this.driveFile({ cid, custom_order: 0, star: "", suffix: "", type: 4 });
  1137. if (!res?.data) return;
  1138.  
  1139. return res.data.map(({ n, pc, fid, cid, t, ...item }) => {
  1140. const _item = { n, pc, fid, cid, t };
  1141. for (const key of keys) _item[key] = item[key];
  1142. return _item;
  1143. });
  1144. }
  1145. static async driveSign() {
  1146. const res = await request(
  1147. "http://115.com/",
  1148. { ct: "offline", ac: "space", _: new Date().getTime() },
  1149. "GET",
  1150. {
  1151. responseType: "json",
  1152. }
  1153. );
  1154. if (res?.state) return { sign: res.sign, time: res.time };
  1155. }
  1156. static driveAddTask({ url, wp_path_id, sign, time }) {
  1157. return request(
  1158. "https://115.com/web/lixian/?ct=lixian&ac=add_task_url",
  1159. { url, wp_path_id, sign, time },
  1160. "POST"
  1161. );
  1162. }
  1163. static driveRename(res) {
  1164. const data = {};
  1165. for (const { fid, file_name } of res) data[`files_new_name[${fid}]`] = file_name;
  1166.  
  1167. return request("https://webapi.115.com/files/batch_rename", data, "POST");
  1168. }
  1169. static driveClear({ pid, fids }) {
  1170. const data = { pid, ignore_warn: 1 };
  1171. fids.forEach(({ fid, cid }, index) => {
  1172. data[`fid[${index}]`] = fid ?? cid;
  1173. });
  1174.  
  1175. return request("https://webapi.115.com/rb/delete", data, "POST");
  1176. }
  1177. static driveInitUpload(params) {
  1178. return request("https://uplb.115.com/3.0/sampleinitupload.php", { userid: "", ...params }, "POST");
  1179. }
  1180. static driveUpload({ host, object, policy, accessid, callback, signature }, blob, name) {
  1181. const formdata = new FormData();
  1182. formdata.append("name", name);
  1183. formdata.append("key", object);
  1184. formdata.append("policy", policy);
  1185. formdata.append("OSSAccessKeyId", accessid);
  1186. formdata.append("success_action_status", 200);
  1187. formdata.append("callback", callback);
  1188. formdata.append("signature", signature);
  1189. formdata.append("file", blob, name);
  1190. return GM_xmlhttpRequest({ method: "POST", url: host, data: formdata });
  1191. }
  1192. static async driveLabel() {
  1193. const res = await request("https://webapi.115.com/label/list?keyword=&limit=115", {}, "GET", {
  1194. responseType: "json",
  1195. });
  1196. if (res?.state) return res.data.list;
  1197. }
  1198. static driveTag({ file_ids, file_label }) {
  1199. return request("https://webapi.115.com/files/batch_label", { file_ids, file_label, action: "add" }, "POST");
  1200. }
  1201. static driveDetail(fid) {
  1202. return request(`https://webapi.115.com/category/get?cid=${fid}`, {}, "GET", {
  1203. responseType: "json",
  1204. });
  1205. }
  1206. }
  1207. class Common {
  1208. menus = [
  1209. {
  1210. title: "站点设置",
  1211. key: "global",
  1212. prefix: "G",
  1213. options: [
  1214. {
  1215. name: "暗黑模式",
  1216. key: "DARK",
  1217. type: "switch",
  1218. info: "常用页面暗黑模式",
  1219. defaultVal: window.matchMedia("(prefers-color-scheme: dark)").matches,
  1220. hotkey: "d",
  1221. },
  1222. {
  1223. name: "快捷搜索",
  1224. key: "SEARCH",
  1225. type: "switch",
  1226. info: "快捷键 <kbd>/</kbd> 获取搜索框焦点,<kbd>Ctrl</kbd> + <kbd>/</kbd> 快速搜索粘贴板首项",
  1227. defaultVal: true,
  1228. hotkey: "k",
  1229. },
  1230. {
  1231. name: "点击事件",
  1232. key: "CLICK",
  1233. type: "switch",
  1234. info: "<code>影片</code> / <code>演员</code> / <code>站点跳转</code> 点击新窗口 (左键前台,右键后台) 打开",
  1235. defaultVal: true,
  1236. hotkey: "c",
  1237. },
  1238. ],
  1239. },
  1240. {
  1241. title: "列表页设置",
  1242. key: "list",
  1243. prefix: "L",
  1244. options: [
  1245. {
  1246. name: "预览替换",
  1247. key: "MIT",
  1248. type: "switch",
  1249. info: "影片预览图替换为封面图",
  1250. defaultVal: true,
  1251. },
  1252. {
  1253. name: "悬停预览",
  1254. key: "MV",
  1255. type: "number",
  1256. 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>',
  1257. placeholder: "仅支持整数 ≥ 0",
  1258. defaultVal: 300,
  1259. },
  1260. {
  1261. name: "标题等高",
  1262. key: "MTH",
  1263. type: "switch",
  1264. info: "影片标题强制等高",
  1265. defaultVal: true,
  1266. },
  1267. {
  1268. name: "标题最大行",
  1269. key: "MTL",
  1270. type: "number",
  1271. info: "影片标题最大显示行数,超出省略。0 不限制 (等高模式下最小有效值 1)",
  1272. placeholder: "仅支持整数 ≥ 0",
  1273. defaultVal: 2,
  1274. },
  1275. {
  1276. name: "滚动加载",
  1277. key: "SCROLL",
  1278. type: "switch",
  1279. info: "滚动加载下一页",
  1280. defaultVal: true,
  1281. },
  1282. {
  1283. name: "合并列表",
  1284. key: "MERGE",
  1285. type: "textarea",
  1286. info: "列表名不可重复,同一列表内地址不可重复 (仅支持影片列表相对地址)<br>合并列表每页按 <code>影片评分</code> (如有) > <code>影片日期</code> > <code>填写顺序</code> 综合排序,并自动去重<br>多列表需以空行分隔",
  1287. placeholder: "[列表1]\n/genre/28#连裤袜\n/star/two#星宮一花\n\n[列表2]\n/\n/uncensored",
  1288. defaultVal: "",
  1289. },
  1290. {
  1291. name: "影片筛选",
  1292. key: "FILTER",
  1293. type: "textarea",
  1294. 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>』',
  1295. placeholder: "[title]\n排泄\n失禁\n\n[code]\n^SSIS-#start\nIPX#start",
  1296. defaultVal: "",
  1297. },
  1298. ],
  1299. },
  1300. {
  1301. title: "详情页设置",
  1302. key: "movie",
  1303. prefix: "M",
  1304. options: [
  1305. {
  1306. name: "在线资源",
  1307. key: "RES",
  1308. type: "input",
  1309. 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>',
  1310. placeholder: "为空时展示页面默认",
  1311. defaultVal: "#video#img#player",
  1312. },
  1313. {
  1314. name: "快捷复制",
  1315. key: "COPY",
  1316. type: "switch",
  1317. info: "为 <code>标题</code> / <code>番号</code> / <code>演员</code> (右键) / <code>磁链</code> 提供快捷复制",
  1318. defaultVal: true,
  1319. },
  1320. {
  1321. name: "标题机翻",
  1322. key: "TITLE",
  1323. type: "switch",
  1324. info: '翻自 <a href="https://translate.google.com/" class="link-primary">Google</a>',
  1325. defaultVal: true,
  1326. },
  1327. {
  1328. name: "演员匹配",
  1329. key: "STAR",
  1330. type: "switch",
  1331. 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>',
  1332. defaultVal: true,
  1333. },
  1334. {
  1335. name: "影片评分",
  1336. key: "SCORE",
  1337. type: "input",
  1338. 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>',
  1339. placeholder: "为空时展示页面默认",
  1340. defaultVal: "#db#lib#dmm",
  1341. },
  1342. {
  1343. name: "站点跳转",
  1344. key: "JUMP",
  1345. type: "textarea",
  1346. info: `番号关键词以 <code>${REP}</code> 表示<br>支持追加参数:<code>#query</code> 预查询资源及字幕,<code>#jump</code> 跳转后自动匹配首项<br>格式参考『<strong>合并列表</strong>』`,
  1347. 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`,
  1348. defaultVal: `[跳转]\nhttps://www.JavLibrary.com/cn/vl_searchbyid.php?keyword=${REP}#query#jump\nhttps://${
  1349. Domain === "JavDB" ? `www.JavBus.com/search/${REP}` : `JavDB.com/search?f=all&q=${REP}&sb=0`
  1350. }#query#jump`,
  1351. },
  1352. {
  1353. name: "磁链最大行",
  1354. key: "LINE",
  1355. type: "number",
  1356. info: "磁力链接最大显示行数,超出以滚动显示。0 不限制",
  1357. placeholder: "仅支持整数 ≥ 0",
  1358. defaultVal: 10,
  1359. },
  1360. {
  1361. name: "字幕筛选",
  1362. key: "SUB",
  1363. type: "switch",
  1364. info: '替换为 <a href="https://javdb.com/" class="link-primary">JavDB</a> 磁链数据',
  1365. defaultVal: false,
  1366. },
  1367. {
  1368. name: "磁链排序",
  1369. key: "SORT",
  1370. type: "switch",
  1371. info: "综合排序 <code>字幕</code> > <code>大小</code> > <code>日期</code>",
  1372. defaultVal: true,
  1373. },
  1374. {
  1375. name: "磁链搜索",
  1376. key: "MAGNET",
  1377. type: "input",
  1378. 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>',
  1379. placeholder: "为空时展示页面默认",
  1380. defaultVal: "#btd#bts#suk",
  1381. },
  1382. ],
  1383. },
  1384. {
  1385. title: "115 相关",
  1386. key: "drive",
  1387. prefix: "D",
  1388. options: [
  1389. {
  1390. name: "网盘资源",
  1391. key: "MATCH",
  1392. type: "switch",
  1393. info: '<strong>需要 <a href="https://115.com/" class="link-primary">网盘</a> 已登录,并调整文件排序方式为 <code>修改时间</code></strong><br>列表页:网盘资源匹配<br>详情页:网盘资源匹配 & (一键) 离线按钮开关<br>(完整体验建议允许弹窗通知权限)',
  1394. defaultVal: true,
  1395. },
  1396. {
  1397. name: "列表离线",
  1398. key: "LOL",
  1399. type: "switch",
  1400. info: "影片列表支持简易『<strong>一键离线</strong>』执行动作:<br>『<strong>磁链搜索</strong>』『<strong>磁链排序</strong>』『<strong>离线验证</strong>』『<strong>最大失败数</strong>』『<strong>离线清理</strong>』『<strong>文件重命名</strong>』",
  1401. defaultVal: true,
  1402. },
  1403. {
  1404. name: "下载目录",
  1405. key: "CID",
  1406. type: "input",
  1407. 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>",
  1408. placeholder: "cid 或 动态目录",
  1409. defaultVal: "${云下载}",
  1410. },
  1411. {
  1412. name: "离线验证",
  1413. key: "VERIFY",
  1414. type: "number",
  1415. info: "添加离线后执行,查询以验证离线下载结果,每次间隔一秒<br>设置验证次数上限,上限次数越多验证越精准<br>建议 3 ~ 5,默认 5",
  1416. placeholder: "仅支持整数 ≥ 0",
  1417. defaultVal: 5,
  1418. },
  1419. {
  1420. name: "最大失败数",
  1421. key: "FAIL",
  1422. type: "number",
  1423. info: "『<strong>一键离线</strong>』时累计验证失败的磁链数,最大值时终止本次任务。0 不限制",
  1424. placeholder: "仅支持整数 ≥ 0",
  1425. defaultVal: 5,
  1426. },
  1427. {
  1428. name: "离线清理",
  1429. key: "CLEAR",
  1430. type: "switch",
  1431. info: "『<strong>离线验证</strong>』成功后执行,匹配番号以清理无关文件",
  1432. defaultVal: true,
  1433. },
  1434. {
  1435. name: "文件重命名",
  1436. key: "RENAME",
  1437. type: "input",
  1438. info: '『<strong>离线验证</strong>』成功后执行,支持参数:<br><code>${字幕}</code> "【中文字幕】",非字幕资源则为空<br><code>${番号}</code> 页面番号 (番号为必须值,如重命名未包含将自动追加前缀)<br><code>${标题}</code> 页面标题,已去除番号<br><code>${序号}</code> 仅作用于视频文件,数字 1 起',
  1439. placeholder: "勿填写后缀,可能导致资源不可用",
  1440. defaultVal: "${字幕}${番号} ${标题}",
  1441. },
  1442. {
  1443. name: "自动打标",
  1444. key: "TAG",
  1445. type: "input",
  1446. info: "『<strong>离线验证</strong>』成功后执行,根据设置值匹配网盘已有标签自动打标,支持参数:<br><code>#genre</code> 类别,<code>#star</code> 演员",
  1447. placeholder: "如 #genre#star",
  1448. defaultVal: "#genre#star",
  1449. },
  1450. {
  1451. name: "图片上传",
  1452. key: "UPLOAD",
  1453. type: "input",
  1454. info: "『<strong>离线验证</strong>』成功后执行 (弹窗通知),支持参数:<br><code>#cover</code> 封面图,<code>#img</code> 预览大图 (通常较大,确保网络通畅并耐心等待上传)",
  1455. placeholder: "如 #cover#img",
  1456. defaultVal: "#cover#img",
  1457. },
  1458. {
  1459. name: "资源调整",
  1460. key: "MODIFY",
  1461. type: "input",
  1462. 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>』",
  1463. placeholder: "如 #rename#clear#tag#upload",
  1464. defaultVal: "#rename#clear#tag#upload",
  1465. },
  1466. ],
  1467. },
  1468. {
  1469. title: "进阶",
  1470. key: "advanced",
  1471. prefix: "A",
  1472. options: [
  1473. {
  1474. name: "资源跳转",
  1475. key: "LINK",
  1476. type: "textarea",
  1477. 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>』",
  1478. placeholder:
  1479. "[left]\nhttps://v.anxia.com/?pickcode=${#pickcode}\n\n[right]\nhttps://115.com/?cid=${#cid}&offset=0&mode=wangpan",
  1480. defaultVal: "",
  1481. },
  1482. ],
  1483. },
  1484. ];
  1485. style = {
  1486. custom: `
  1487. :root {
  1488. --x-bgc: #121212;
  1489. --x-sub-bgc: #202020;
  1490. --x-ftc: #fffffff2;
  1491. --x-sub-ftc: #aaa;
  1492. --x-grey: #313131;
  1493. --x-blue: #0a84ff;
  1494. --x-orange: #ff9f0a;
  1495. --x-green: #30d158;
  1496. --x-red: #ff453a;
  1497. --x-yellow: #ffd60a;
  1498. --x-line-h: 22px;
  1499. --x-thumb-w: 190px;
  1500. --x-cover-w: 360px;
  1501. --x-thumb-ratio: 147 / 200;
  1502. --x-cover-ratio: 400 / 269;
  1503. --x-avatar-ratio: 1;
  1504. --x-sprite-ratio: 4 / 3;
  1505. }
  1506. .x-hide, .x-scrollbar-hide body::-webkit-scrollbar {
  1507. display: none !important;
  1508. }
  1509. .x-show {
  1510. display: block !important;
  1511. }
  1512. #x-mask {
  1513. position: fixed;
  1514. top: 0;
  1515. left: 0;
  1516. z-index: 9999;
  1517. display: none;
  1518. box-sizing: border-box;
  1519. width: 100vw;
  1520. height: 100vh;
  1521. margin: 0;
  1522. padding: 0;
  1523. background: transparent;
  1524. border: none;
  1525. }
  1526. .x-in {
  1527. opacity: 1 !important;
  1528. transition: opacity .25s linear !important;
  1529. }
  1530. .x-cover {
  1531. width: var(--x-cover-w) !important;
  1532. }
  1533. .x-cover > :first-child {
  1534. aspect-ratio: var(--x-cover-ratio) !important;
  1535. }
  1536. .x-cover img {
  1537. object-fit: contain !important;
  1538. }
  1539. .x-ellipsis {
  1540. display: -webkit-box !important;
  1541. overflow: hidden;
  1542. white-space: unset !important;
  1543. text-overflow: ellipsis;
  1544. -webkit-line-clamp: 1;
  1545. -webkit-box-orient: vertical;
  1546. word-break: break-all;
  1547. }
  1548. .x-line {
  1549. overflow: hidden;
  1550. white-space: nowrap;
  1551. text-overflow: ellipsis;
  1552. }
  1553. #x-status {
  1554. margin-bottom: 20px;
  1555. color: var(--x-sub-ftc);
  1556. font-size: 14px !important;
  1557. text-align: center;
  1558. }
  1559. .x-highlight {
  1560. box-shadow: 0 0 0 4px var(--x-red) !important;
  1561. }
  1562. .x-ml {
  1563. margin-left: 10px;
  1564. }
  1565. .x-side {
  1566. position: absolute;
  1567. top: 50%;
  1568. z-index: 7;
  1569. display: flex;
  1570. align-items: center;
  1571. justify-content: center;
  1572. width: 60px;
  1573. height: 60px;
  1574. color: var(--x-sub-bgc);
  1575. font-size: 30px;
  1576. background: #fff;
  1577. border-radius: 50%;
  1578. transform: translateY(-50%);
  1579. cursor: pointer;
  1580. opacity: .4;
  1581. transition: opacity .2s linear;
  1582. user-select: none;
  1583. }
  1584. .x-side:hover {
  1585. opacity: .8;
  1586. }
  1587. .x-side > * {
  1588. top: 0;
  1589. }
  1590. .x-side.prev {
  1591. left: 30px;
  1592. }
  1593. .x-side.next {
  1594. right: 30px;
  1595. }
  1596. .x-flex-center {
  1597. display: flex !important;
  1598. align-items: center;
  1599. }
  1600. .x-grid {
  1601. display: grid !important;
  1602. }
  1603. *:has(> .x-jump) {
  1604. margin-right: -5px !important;
  1605. }
  1606. .x-jump {
  1607. min-width: 44px;
  1608. margin: 0 5px 5px 0 !important;
  1609. cursor: pointer;
  1610. }
  1611. .x-title {
  1612. line-height: var(--x-line-h) !important;
  1613. }
  1614. *:has(> .x-player) .x-title {
  1615. color: var(--x-blue) !important;
  1616. font-weight: bold;
  1617. }
  1618. .x-preview,
  1619. .x-loading *:has(> img),
  1620. .x-player {
  1621. position: relative;
  1622. display: block;
  1623. overflow: hidden;
  1624. }
  1625. .x-loading *:has(> img)::before,
  1626. .x-player::before,
  1627. .x-player::after {
  1628. position: absolute;
  1629. top: 50%;
  1630. left: 50%;
  1631. cursor: pointer;
  1632. opacity: .8;
  1633. content: "";
  1634. }
  1635. .x-loading *:has(> img):not(.x-player)::before,
  1636. .x-player::before {
  1637. z-index: 8;
  1638. width: 60px;
  1639. height: 60px;
  1640. background: #0a5ee0;
  1641. border-radius: 50%;
  1642. filter: drop-shadow(2px 5px 10px rgb(0 0 0 / 50%));
  1643. translate: -50% -50%;
  1644. }
  1645. .x-loading *:has(> img)::before,
  1646. .x-zh.x-player::before {
  1647. border: 6px solid var(--x-yellow);
  1648. }
  1649. .x-player::after {
  1650. z-index: 9;
  1651. border-top: 15px solid transparent;
  1652. border-bottom: 15px solid transparent;
  1653. border-left: 30px solid #fff;
  1654. transform: translate(-40%, -50%);
  1655. }
  1656. .x-loading *:has(> img)::before {
  1657. border-color: transparent;
  1658. border-bottom-color: var(--x-orange) !important;
  1659. animation: rotation 1s linear infinite;
  1660. }
  1661. .x-loading *:has(> img):not(.x-player)::before {
  1662. background: transparent;
  1663. border-color: #000c;
  1664. }
  1665. .x-loading.success *:has(> img)::before {
  1666. border-color: var(--x-green) !important;
  1667. }
  1668. .x-loading.timeout *:has(> img)::before {
  1669. border-color: var(--x-orange) !important;
  1670. }
  1671. .x-loading.fail *:has(> img)::before {
  1672. border-color: var(--x-red) !important;
  1673. }
  1674. @keyframes rotation {
  1675. 0% {
  1676. rotate: 0deg;
  1677. }
  1678. 100% {
  1679. rotate: 360deg;
  1680. }
  1681. }
  1682. .x-preview :is(img, video) {
  1683. position: absolute;
  1684. top: 0;
  1685. left: 0;
  1686. width: 100%;
  1687. height: 100%;
  1688. object-fit: contain;
  1689. }
  1690. .x-preview video {
  1691. z-index: 10;
  1692. background-color: inherit;
  1693. opacity: 0;
  1694. transition: opacity .25s ease-in !important;
  1695. }
  1696. .x-preview video::-webkit-media-controls-fullscreen-button {
  1697. display: none;
  1698. }
  1699. .x-btn {
  1700. display: none;
  1701. margin-right: 4px;
  1702. padding: 1px 4px;
  1703. color: #fff !important;
  1704. font-weight: normal;
  1705. font-size: calc(1em - 2px);
  1706. background: var(--x-blue);
  1707. border-radius: 2px;
  1708. cursor: pointer;
  1709. }
  1710. .x-btn::after {
  1711. content: "调整";
  1712. }
  1713. .x-btn.active {
  1714. cursor: wait;
  1715. opacity: .8;
  1716. }
  1717. .x-btn.active::after {
  1718. content: "请求中...";
  1719. }
  1720. .x-btn.pending::after {
  1721. content: "校验中...";
  1722. }
  1723. `,
  1724. 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}",
  1725. 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}",
  1726. };
  1727.  
  1728. init() {
  1729. Store.init();
  1730. const key = Object.keys(this.routes).find(key => this.routes[key].test(location.pathname));
  1731. this.registerMenu(key);
  1732. return { ...this, ...this[key] };
  1733. }
  1734. registerMenu(active) {
  1735. const exclude = this.excludeMenu ?? [];
  1736. const menus = this.menus.filter(item => !exclude.includes(`${item.prefix}_`));
  1737. const { length } = menus;
  1738. if (!length) return;
  1739.  
  1740. let tabStr = "";
  1741. let panelStr = "";
  1742. const commands = [];
  1743. if (!menus.some(item => item.key === active)) active = menus[0].key;
  1744. for (let index = 0; index < length; index++) {
  1745. const { title, key, prefix, options } = menus[index];
  1746.  
  1747. let sections = "";
  1748. for (let idx = 0, len = options.length; idx < len; idx++) {
  1749. let { name, key: curKey, type, info, placeholder = "", defaultVal, hotkey = "" } = options[idx];
  1750. curKey = `${prefix}_${curKey}`;
  1751. if (exclude.includes(curKey)) continue;
  1752.  
  1753. commands.push(curKey);
  1754. const uniKey = `${Domain}_${curKey}`;
  1755. const val = GM_getValue(uniKey, defaultVal);
  1756. this[curKey] = val;
  1757.  
  1758. let section = "";
  1759. if (type === "switch") {
  1760. if (hotkey && hotkey !== "s") {
  1761. GM_registerMenuCommand(
  1762. `${val ? "关闭" : "开启"}${name}`,
  1763. () => {
  1764. GM_setValue(uniKey, !val);
  1765. location.reload();
  1766. },
  1767. hotkey
  1768. );
  1769. }
  1770. section = `
  1771. <div class="form-check form-switch">
  1772. <input
  1773. type="checkbox"
  1774. class="form-check-input"
  1775. role="switch"
  1776. id="${curKey}"
  1777. aria-describedby="${curKey}_Help"
  1778. ${val ? "checked" : ""}
  1779. name="${curKey}"
  1780. >
  1781. <label class="form-check-label" for="${curKey}">${name}</label>
  1782. </div>
  1783. `;
  1784. } else if (type === "textarea") {
  1785. section = `
  1786. <label class="form-label" for="${curKey}">${name}</label>
  1787. <textarea
  1788. rows="5"
  1789. class="form-control"
  1790. id="${curKey}"
  1791. aria-describedby="${curKey}_Help"
  1792. placeholder="${placeholder}"
  1793. name="${curKey}"
  1794. >${val ?? ""}</textarea>
  1795. `;
  1796. } else {
  1797. section = `
  1798. <label class="form-label" for="${curKey}">${name}</label>
  1799. <input
  1800. type="${type}"
  1801. class="form-control"
  1802. id="${curKey}"
  1803. aria-describedby="${curKey}_Help"
  1804. value="${val ?? ""}"
  1805. placeholder="${placeholder}"
  1806. name="${curKey}"
  1807. >
  1808. `;
  1809. }
  1810. if (info) section += `<div id="${curKey}_Help" class="form-text">${info}</div>`;
  1811. sections += `<div class="mb-3">${section}</div>`;
  1812. }
  1813. if (!sections) continue;
  1814.  
  1815. const isActive = key === active;
  1816. tabStr += `
  1817. <button
  1818. class="nav-link${isActive ? " active" : ""} text-start"
  1819. id="${key}-tab"
  1820. data-bs-toggle="pill"
  1821. data-bs-target="#${key}"
  1822. type="button"
  1823. role="tab"
  1824. aria-controls="${key}"
  1825. aria-selected="${isActive}"
  1826. >${title}</button>
  1827. `;
  1828. panelStr += `
  1829. <div
  1830. class="tab-pane fade${isActive ? " show active" : ""}"
  1831. id="${key}"
  1832. role="tabpanel"
  1833. aria-labelledby="${key}-tab"
  1834. tabindex="0"
  1835. >
  1836. ${sections}
  1837. </div>
  1838. `;
  1839. }
  1840. if (!tabStr || !panelStr) return;
  1841.  
  1842. GM_addStyle(this.style.custom);
  1843. DOC.addEventListener(
  1844. "XContentLoaded",
  1845. () => {
  1846. const title = `${GM_info.script.name} v${GM_info.script.version}`;
  1847. DOC.body.insertAdjacentHTML(
  1848. "beforeend",
  1849. `<iframe id="x-mask" src="about:blank" title="${title}"></iframe>`
  1850. );
  1851. const iframe = DOC.querySelector("#x-mask");
  1852. const _DOC = iframe.contentWindow.document;
  1853. const { body } = _DOC;
  1854.  
  1855. _DOC.head.insertAdjacentHTML(
  1856. "beforeend",
  1857. `<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/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">`
  1858. );
  1859. GM_addElement(body, "script", {
  1860. src: "https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.min.js",
  1861. integrity: "sha384-IDwe1+LCz02ROU9k972gdyvl+AESN10+x7tBKgc9I5HFtuNz0wWnPclzo6p9vxnk",
  1862. crossorigin: "anonymous",
  1863. });
  1864.  
  1865. body.classList.add("bg-transparent");
  1866. body.insertAdjacentHTML(
  1867. "afterbegin",
  1868. `<button
  1869. id="openControlPanel"
  1870. type="button"
  1871. class="d-none"
  1872. data-bs-toggle="modal"
  1873. data-bs-target="#controlPanel"
  1874. >openControlPanel</button>
  1875. <div
  1876. class="modal fade"
  1877. id="controlPanel"
  1878. tabindex="-1"
  1879. aria-labelledby="controlPanelLabel"
  1880. aria-hidden="true"
  1881. >
  1882. <div class="modal-dialog modal-lg modal-fullscreen-lg-down modal-dialog-scrollable">
  1883. <div class="modal-content">
  1884. <div class="modal-header">
  1885. <h5 class="modal-title fs-5" id="controlPanelLabel">控制面板
  1886. - <a
  1887. href="https://sleazyfork.org/zh-CN/scripts/435360-javscript"
  1888. class="link-secondary"
  1889. >${title}</a>
  1890. </h5>
  1891. <button
  1892. type="button"
  1893. class="btn-close"
  1894. data-bs-dismiss="modal"
  1895. aria-label="Close"
  1896. ></button>
  1897. </div>
  1898. <div class="modal-body">
  1899. <form class="mb-0">
  1900. <div class="d-flex align-items-start">
  1901. <div
  1902. class="nav flex-column nav-pills me-4 sticky-top"
  1903. id="v-pills-tab"
  1904. role="tablist"
  1905. aria-orientation="vertical"
  1906. >
  1907. ${tabStr}
  1908. </div>
  1909. <div class="tab-content flex-fill" id="v-pills-tabContent">
  1910. ${panelStr}
  1911. </div>
  1912. </div>
  1913. </form>
  1914. </div>
  1915. <div class="modal-footer">
  1916. <div class="flex-fill">
  1917. <div class="btn-group" role="group" aria-label="Import and Export">
  1918. <button type="button" class="btn btn-link" data-action="import">导入</button>
  1919. <button type="button" class="btn btn-link" data-action="export">导出</button>
  1920. </div>
  1921. </div>
  1922. <button
  1923. type="button"
  1924. class="btn btn-danger"
  1925. data-bs-dismiss="modal"
  1926. data-action="restart"
  1927. >重置脚本</button>
  1928. <button
  1929. type="button"
  1930. class="btn btn-secondary"
  1931. data-bs-dismiss="modal"
  1932. data-action="clear"
  1933. >清除缓存</button>
  1934. <button
  1935. type="button"
  1936. class="btn btn-secondary"
  1937. data-bs-dismiss="modal"
  1938. data-action="reset"
  1939. >默认设置</button>
  1940. <button
  1941. type="button"
  1942. class="btn btn-primary"
  1943. data-bs-dismiss="modal"
  1944. data-action="save"
  1945. >保存设置</button>
  1946. </div>
  1947. </div>
  1948. </div>
  1949. </div>`
  1950. );
  1951. body.querySelector("#controlPanel .modal-footer").addEventListener("click", e => {
  1952. const { action } = e.target.dataset;
  1953. if (!action) return;
  1954.  
  1955. e.preventDefault();
  1956. e.stopPropagation();
  1957.  
  1958. const prefixes = MatchDomains.slice(0, -1).map(item => item.domain);
  1959. if (action === "import") {
  1960. const upConfig = file => {
  1961. const fileReader = new FileReader();
  1962. fileReader.readAsText(file);
  1963. fileReader.onload = () => {
  1964. for (const [key, val] of Object.entries(JSON.parse(fileReader.result))) {
  1965. if (prefixes.includes(key.split("_")[0])) GM_setValue(key, val);
  1966. }
  1967. location.reload();
  1968. };
  1969. };
  1970.  
  1971. const upload = DOC.create("input", { type: "file", accept: ".json" });
  1972. upload.addEventListener(
  1973. "change",
  1974. e => {
  1975. const file = e.target.files[0];
  1976. if (file?.type !== "application/json") return;
  1977. if (file.name.split(".json")[0] === title) return upConfig(file);
  1978. if (window.confirm("导入配置可能与当前版本不符,是否继续?")) return upConfig(file);
  1979. },
  1980. false
  1981. );
  1982. return upload.click();
  1983. }
  1984. if (action === "export") {
  1985. const config = {};
  1986. GM_listValues().forEach(key => {
  1987. if (prefixes.includes(key.split("_")[0])) config[key] = GM_getValue(key);
  1988. });
  1989. const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" });
  1990.  
  1991. const download = DOC.create("a", {
  1992. download: `${title}.json`,
  1993. href: URL.createObjectURL(blob),
  1994. });
  1995. return download.click();
  1996. }
  1997. if (action === "save") {
  1998. const data = Object.fromEntries(
  1999. new FormData(body.querySelector("#controlPanel form")).entries()
  2000. );
  2001. commands.forEach(key => GM_setValue(`${Domain}_${key}`, data[key] ?? ""));
  2002. }
  2003. if (action === "reset") {
  2004. GM_listValues().forEach(name => name.startsWith(Domain) && GM_deleteValue(name));
  2005. }
  2006. if (action === "clear") {
  2007. GM_setValue("DETAILS", {});
  2008. GM_setValue("RESOURCE", {});
  2009. localStorage.clear();
  2010. }
  2011. if (action === "restart") GM_listValues().forEach(name => GM_deleteValue(name));
  2012.  
  2013. location.reload();
  2014. });
  2015.  
  2016. const toggleIframe = () => {
  2017. DOC.body.parentNode.classList.toggle("x-scrollbar-hide");
  2018. iframe.classList.toggle("x-show");
  2019. };
  2020. const openModal = () => {
  2021. if (iframe.classList.contains("x-show")) return;
  2022. toggleIframe();
  2023. _DOC.querySelector("#openControlPanel").click();
  2024. };
  2025. GM_registerMenuCommand("控制面板", openModal, "s");
  2026. _DOC.querySelector("#controlPanel").addEventListener("hidden.bs.modal", toggleIframe);
  2027. },
  2028. { once: true }
  2029. );
  2030. }
  2031.  
  2032. // G_DARK
  2033. globalDark = (style = "", dmStyle = "") => {
  2034. if (!style && !dmStyle) return;
  2035.  
  2036. const { common, dark } = this.style;
  2037. if (style && common) style = `${common}${style}`;
  2038. if (!this.G_DARK) dmStyle = "";
  2039. if (dmStyle && dark) dmStyle = `${dark}${dmStyle}`;
  2040.  
  2041. const css = `${style}${dmStyle}`;
  2042. if (css) GM_addStyle(css);
  2043. };
  2044. // G_SEARCH
  2045. globalSearch = () => {
  2046. if (!this.G_SEARCH) return;
  2047.  
  2048. const { selectors, pathname } = this.search;
  2049. if (selectors) {
  2050. DOC.addEventListener("keyup", ({ code }) => {
  2051. if (code !== "Slash" || ["INPUT", "TEXTAREA"].includes(DOC.activeElement.nodeName)) return;
  2052. DOC.querySelector(selectors)?.focus();
  2053. });
  2054. }
  2055. if (pathname) {
  2056. DOC.addEventListener("keydown", async e => {
  2057. if (e.ctrlKey && e.code === "Slash") {
  2058. const text = await navigator.clipboard.readText();
  2059. if (text) openInTab(`${location.origin}${pathname}#jump#${REP}`.replaceAll(REP, text));
  2060. }
  2061. });
  2062. }
  2063. };
  2064. // G_CLICK
  2065. globalClick = () => {
  2066. const { selectors, codeQuery, upQuery } = this.click;
  2067. if (!selectors?.length) return;
  2068.  
  2069. selectors.push(".x-jump");
  2070. const getTarget = ({ target }) => target.closest(selectors) || false;
  2071.  
  2072. DOC.addEventListener("click", e => {
  2073. const closest = getTarget(e);
  2074. if (!closest) return;
  2075.  
  2076. let url = "";
  2077. let code = "";
  2078. const { classList, dataset } = e.target;
  2079.  
  2080. if (classList.contains("x-player") && dataset.pc) {
  2081. return this.advancedLink(e, { type: "left" });
  2082. } else if (classList.contains("x-btn") && dataset.code) {
  2083. return this.driveListOffLine(e, dataset);
  2084. } else if (classList.contains("x-jump") && dataset.href) {
  2085. url = dataset.href;
  2086. } else if (closest.href) {
  2087. url = closest.href;
  2088. if (codeQuery) code = closest.querySelector(codeQuery)?.textContent;
  2089. }
  2090. if (!url) return;
  2091.  
  2092. e.preventDefault();
  2093. e.stopPropagation();
  2094. if (this.G_CLICK) {
  2095. const tab = openInTab(url);
  2096. if (code) tab.onclose = () => this.driveMatchForList([{ item: closest, code }], upQuery);
  2097. return;
  2098. }
  2099. location.href = url;
  2100. });
  2101. if (!this.G_CLICK) return;
  2102.  
  2103. let _event;
  2104. DOC.addEventListener("mousedown", e => {
  2105. if (e.button !== 2) return;
  2106.  
  2107. const target = getTarget(e);
  2108. if (!target) return;
  2109.  
  2110. e.preventDefault();
  2111. target.oncontextmenu = e => e.preventDefault();
  2112. _event = e;
  2113. });
  2114. DOC.addEventListener("mouseup", e => {
  2115. if (e.button !== 2 || !_event) return;
  2116.  
  2117. const target = getTarget(e);
  2118. if (!target) return;
  2119.  
  2120. e.preventDefault();
  2121. const { clientX, clientY } = e;
  2122. const { clientX: _clientX, clientY: _clientY } = _event;
  2123. if (Math.abs(clientX - _clientX) + Math.abs(clientY - _clientY) > 5) return;
  2124. if (target.dataset.href) return openInTab(target.dataset.href, false);
  2125.  
  2126. const tab = openInTab(target.href, false);
  2127. const code = codeQuery ? target.querySelector(codeQuery)?.textContent : "";
  2128. if (code) tab.onclose = () => this.driveMatchForList([{ item: target, code }], upQuery);
  2129. });
  2130. };
  2131.  
  2132. // L_MIT
  2133. listMovieImgType = (node, condition) => {
  2134. if (!this.L_MIT) return;
  2135.  
  2136. const img = node.querySelector("img");
  2137. if (!img) return;
  2138.  
  2139. node.classList.add("x-cover");
  2140. const { src = "" } = img;
  2141. img.src = condition.find(({ regex }) => regex.test(src))?.replace(src) ?? src;
  2142. };
  2143. // L_MV
  2144. listMovieVideo = container => {
  2145. if (!container.querySelector(".movie-list:is(.h, .cols-3) .cover, .movie-box.x-cover")) return;
  2146.  
  2147. const DELAY = parseInt(this.L_MV ?? 0, 10);
  2148. if (DELAY <= 0) return;
  2149.  
  2150. let locking = null;
  2151. unsafeWindow.addEventListener("scroll", () => {
  2152. if (locking) clearTimeout(locking);
  2153.  
  2154. if (!container.style.pointerEvents) container.style.pointerEvents = "none";
  2155.  
  2156. locking = setTimeout(() => {
  2157. container.style.pointerEvents = "";
  2158. }, 100);
  2159. });
  2160.  
  2161. const { selectors, codeQuery, upQuery } = this.click;
  2162. const ITEM_NODE = selectors[0];
  2163. const LOCK_NODE = "x-loading";
  2164. const HOLD_NODE = "x-holding";
  2165. let current = null;
  2166. let timer = null;
  2167.  
  2168. DOC.addEventListener("mouseover", ({ relatedTarget, target }) => {
  2169. if (current) {
  2170. if (timer && relatedTarget?.nodeName === "IMG" && target.contains(relatedTarget)) {
  2171. clearTimeout(timer);
  2172. current = null;
  2173. }
  2174. return;
  2175. }
  2176.  
  2177. target = target.closest(upQuery);
  2178. if (!target) return;
  2179.  
  2180. if (!container.contains(target)) return;
  2181.  
  2182. current = target;
  2183. showPreview(current);
  2184. });
  2185. DOC.addEventListener("mouseout", ({ relatedTarget }) => {
  2186. if (!current) return;
  2187.  
  2188. while (relatedTarget) {
  2189. if (relatedTarget === current) return;
  2190. relatedTarget = relatedTarget.parentNode;
  2191. }
  2192.  
  2193. hidePreview(current);
  2194. current = null;
  2195. });
  2196.  
  2197. const showPreview = cover => {
  2198. cover.classList.add("x-preview", HOLD_NODE);
  2199. timer = setTimeout(() => getVideo(cover), DELAY < 100 ? 100 : DELAY);
  2200. };
  2201. const hidePreview = cover => {
  2202. cover.classList.remove(HOLD_NODE);
  2203. if (timer) clearTimeout(timer);
  2204.  
  2205. const video = cover.querySelector("video");
  2206. if (!video) return;
  2207.  
  2208. video.classList.remove("x-in");
  2209. setTimeout(() => video.remove(), 250);
  2210. };
  2211.  
  2212. let isMuted = true;
  2213. const getVideo = cover => {
  2214. const closest = cover.closest(ITEM_NODE);
  2215. if (!closest || closest.classList.contains(LOCK_NODE)) return;
  2216.  
  2217. let code = closest.querySelector(codeQuery)?.textContent.trim() ?? "";
  2218. if (!code) return;
  2219.  
  2220. code = /^FC2-\d/i.test(code) ? code.replace("-", "-PPV-") : code;
  2221. let { video, poster } = cover.dataset;
  2222.  
  2223. if (!video) {
  2224. const detail = Store.getDetail(code);
  2225. if (detail.video) {
  2226. video = detail.video;
  2227. cover.setAttribute("data-video", video);
  2228. }
  2229. }
  2230. if (!poster) {
  2231. poster = cover.querySelector("img")?.src ?? "";
  2232. cover.setAttribute("data-poster", poster);
  2233. }
  2234. if (video) return setVideo(cover);
  2235.  
  2236. closest.classList.add(LOCK_NODE);
  2237. const _timer = setTimeout(() => cancelLock("timeout"), 8000);
  2238. let count = FC2_REGEX.test(code) ? 7 : 6;
  2239.  
  2240. const cancelLock = status => {
  2241. if (_timer) clearTimeout(_timer);
  2242. count = 0;
  2243.  
  2244. if (status === "success" && cover.classList.contains(HOLD_NODE)) {
  2245. return closest.classList.remove(LOCK_NODE);
  2246. }
  2247. closest.classList.add(status);
  2248. setTimeout(() => closest.classList.remove(status, LOCK_NODE), 800);
  2249. };
  2250. const callback = ({ video }) => {
  2251. if (count <= 0) return;
  2252.  
  2253. if (!video) {
  2254. count--;
  2255. if (count <= 0) cancelLock("fail");
  2256. } else {
  2257. cancelLock("success");
  2258. cover.setAttribute("data-video", video);
  2259. Store.upDetail(code, { video });
  2260. setVideo(cover);
  2261. }
  2262. };
  2263.  
  2264. if (count === 7) Apis.fetchFC2(code).then(callback);
  2265. Apis.fetchAVPreview(code).then(callback);
  2266. Apis.fetchJavSpyl(code).then(callback);
  2267. Apis.fetchVBGFL(code).then(callback);
  2268. Apis.fetchDMM({ code }).then(({ fetchVideo }) => {
  2269. if (fetchVideo) fetchVideo().then(callback);
  2270. });
  2271.  
  2272. const isVR = VR_REGEX.test(closest.textContent);
  2273. Apis.fetchVideoByGuess({ code, isVR }).then(callback);
  2274. Apis.fetchVBL({ node: closest, isVR }).then(callback);
  2275. };
  2276. const setVideo = cover => {
  2277. if (!cover.classList.contains(HOLD_NODE) || cover.querySelector("video")) return;
  2278.  
  2279. let { video, poster } = cover.dataset;
  2280. video = DOC.create("video", {
  2281. poster,
  2282. src: video,
  2283. "x-webkit-airplay": "deny",
  2284. controlslist: "nodownload nofullscreen noremoteplayback noplaybackrate",
  2285. preload: "metadata",
  2286. title: "",
  2287. });
  2288. video.autoplay = true;
  2289. video.autoPictureInPicture = false;
  2290. video.controls = true;
  2291. video.currentTime = 3;
  2292. video.disablePictureInPicture = true;
  2293. video.disableRemotePlayback = true;
  2294. video.loop = true;
  2295. video.muted = isMuted;
  2296. video.playsInline = true;
  2297. video.volume = localStorage.getItem("volume") ?? 0.2;
  2298. video.addEventListener("keyup", ({ code, target }) => {
  2299. if (code !== "KeyM") return;
  2300. isMuted = !target.muted;
  2301. target.muted = isMuted;
  2302. });
  2303. cover.append(video);
  2304. video?.focus();
  2305. video?.load();
  2306. setTimeout(() => video?.classList?.add("x-in"), 50);
  2307. };
  2308. };
  2309. // L_MTH, L_MTL
  2310. listMovieTitle = () => {
  2311. let num = parseInt(this.L_MTL ?? 0, 10);
  2312. if (this.L_MTH && num < 1) num = 1;
  2313.  
  2314. return `.x-ellipsis {
  2315. -webkit-line-clamp: ${num <= 0 ? "unset" : num};
  2316. ${this.L_MTH ? `height: calc(var(--x-line-h) * ${num}) !important;` : ""}
  2317. }`;
  2318. };
  2319. // L_SCROLL
  2320. listScroll = async function ({ container, itemSelector = ".item", path, start, showPage, onLoad }) {
  2321. let { list: domList = [DOC], active: isMerge = false } = this.merge;
  2322.  
  2323. const getItems = async list => {
  2324. list = await Promise.all(list.map(item => (typeof item !== "string" ? item : request(item))));
  2325. list = list.filter(Boolean);
  2326. if (!list.length) return list;
  2327.  
  2328. domList = list;
  2329. list = list.map(item => [...item.querySelectorAll(itemSelector)]).flat();
  2330. if (!list.length) return list;
  2331.  
  2332. return this.modifyItem(list, isMerge);
  2333. };
  2334. const items = await getItems(domList);
  2335. const { length } = items;
  2336. start(items);
  2337.  
  2338. if (typeof container === "string") container = DOC.querySelector(container);
  2339. container.classList.add("x-in");
  2340.  
  2341. if (length) this.listMovieVideo(container);
  2342. if (!isMerge && !this.L_SCROLL) return showPage();
  2343.  
  2344. const hasMore = "加载中...";
  2345. const noMore = "没有更多了";
  2346. const status = DOC.create("div", { id: "x-status" }, length ? hasMore : noMore);
  2347. container.insertAdjacentElement("afterend", status);
  2348. if (!length) return;
  2349.  
  2350. let urls = [];
  2351. const getNext = list => {
  2352. urls = list.map(item => item.querySelector(path)?.href).filter(Boolean);
  2353. addPrefetch(urls);
  2354. };
  2355. getNext(domList);
  2356. if (!urls.length) {
  2357. status.textContent = noMore;
  2358. return;
  2359. }
  2360.  
  2361. let isLoading = false;
  2362. let observer = new IntersectionObserver(async entries => {
  2363. if (!urls.length) {
  2364. status.textContent = noMore;
  2365. return observer.disconnect();
  2366. }
  2367. if (!entries[0].isIntersecting || isLoading) return;
  2368. status.textContent = hasMore;
  2369.  
  2370. isLoading = true;
  2371. const _items = await getItems(urls);
  2372. isLoading = false;
  2373.  
  2374. if (!_items.length) {
  2375. status.textContent = "请求失败,检查后滚动以重试";
  2376. return;
  2377. }
  2378. getNext(domList);
  2379. onLoad(_items);
  2380. });
  2381. observer.observe(status);
  2382. };
  2383. // L_MERGE
  2384. listMerge = () => {
  2385. let merge = this.L_MERGE.trim();
  2386. if (!merge) return;
  2387.  
  2388. merge = merge.split("\n\n").filter(Boolean);
  2389. if (!merge.length) return;
  2390.  
  2391. const list = {};
  2392. const regex = /^\[.+\]$/;
  2393. const replace = /\[|\]/g;
  2394. for (const item of merge) {
  2395. const res = unique(item.split("\n").filter(Boolean));
  2396. if (res.length <= 1) continue;
  2397.  
  2398. const first = res.shift();
  2399. if (!regex.test(first)) continue;
  2400.  
  2401. list[first.replace(replace, "")] = res;
  2402. }
  2403. const keys = Object.keys(list);
  2404. if (!keys.length) return;
  2405.  
  2406. let active = "";
  2407. const { activePath = "/", start } = this.merge;
  2408. if (location.pathname === activePath) {
  2409. const { search } = location;
  2410. if (search.startsWith("?merge=")) {
  2411. active = decodeURIComponent(search.split("=").at(-1));
  2412. if (!keys.includes(active)) active = "";
  2413. }
  2414. }
  2415. DOC.addEventListener("XContentLoaded", () => start(keys, active), { once: true });
  2416. if (!active) return;
  2417.  
  2418. this.merge.active = active;
  2419. this.merge.list = list[active].map(item => (item.split("#")[0] === activePath ? DOC : item));
  2420. DOC.title = `${active} - 合并列表 - ${Domain}`;
  2421. };
  2422. // L_FILTER
  2423. listFilter = ({ code = "", title = "", score, tags }) => {
  2424. const filter = this.L_FILTER.trim();
  2425. if (!filter || (!code && !title)) return;
  2426.  
  2427. const hlRegex = /^\^/;
  2428. tags = tags?.length
  2429. ? Array.from(tags)
  2430. .map(item => item.textContent)
  2431. .join(",")
  2432. : "";
  2433. const match = (rule, word) => {
  2434. if (rule.endsWith("#start")) {
  2435. return word.startsWith(rule.replace(/#start$/, ""));
  2436. } else if (rule.endsWith("#end")) {
  2437. return word.endsWith(rule.replace(/#end$/, ""));
  2438. } else {
  2439. if (!rule.startsWith("reg:")) return word.includes(rule);
  2440.  
  2441. rule = rule.replace(/^reg:/, "");
  2442. return rule ? eval(rule)?.test(word) : rule;
  2443. }
  2444. };
  2445.  
  2446. let res = "";
  2447. for (const item of filter.split("\n\n")) {
  2448. let word = "";
  2449. if (item.startsWith("[code]")) word = code;
  2450. if (item.startsWith("[title]")) word = title;
  2451. if (item.startsWith("[score]")) word = score;
  2452. if (item.startsWith("[tags]")) word = tags;
  2453. if (!word && word !== "") continue;
  2454.  
  2455. const rules = unique(item.split("\n").filter(Boolean));
  2456. if (rules.length <= 1) continue;
  2457.  
  2458. const highlight = [];
  2459. for (const rule of rules.slice(1)) {
  2460. if (hlRegex.test(rule)) {
  2461. highlight.push(rule.replace(hlRegex, ""));
  2462. continue;
  2463. }
  2464. if (match(rule, word)) res = "x-hide";
  2465. if (res === "x-hide") break;
  2466. }
  2467. if (res === "x-hide") break;
  2468.  
  2469. for (const rule of highlight) {
  2470. if (match(rule, word)) res = "x-highlight";
  2471. if (res === "x-highlight") break;
  2472. }
  2473. }
  2474. return res;
  2475. };
  2476.  
  2477. // M_RES, M_TITLE, M_STAR, M_SCORE, M_SUB, M_MAGNET
  2478. getMovieResource = function ({ video, transTitle, star, code, cid, studio, isVR, title }) {
  2479. let res = [];
  2480.  
  2481. if (this.M_RES?.trim()) {
  2482. res = paramParse(this.M_RES, ["img", "video", "player"]);
  2483. this.params.res = res;
  2484. }
  2485. if (this.M_SCORE?.trim()) {
  2486. const param = paramParse(this.M_SCORE, ["db", "lib", "dmm", "mgs"]);
  2487. const _res = param.filter(item => Domain !== "JavDB" || item !== "db").map(item => `${item}_score`);
  2488. res = [...res, ..._res];
  2489. if (_res.length) this.params.score = param;
  2490. }
  2491. if (this.M_MAGNET?.trim()) {
  2492. const param = paramParse(this.M_MAGNET, ["suk", "bts", "btd"]);
  2493. res = [...res, ...param.map(item => `${item}_magnet`)];
  2494. this.params.magnet = param;
  2495. }
  2496. if (this.M_TITLE) res.push("title");
  2497. if (!star?.length && this.M_STAR) res.push("star");
  2498. if (this.M_SUB) res.push("sub");
  2499.  
  2500. const needs = [];
  2501. const detail = Store.getDetail(code);
  2502. const resource = { video, title: transTitle };
  2503. for (const key of res) {
  2504. let val = detail[key];
  2505. this.params[key] = val;
  2506. if (val?.length) continue;
  2507.  
  2508. val = resource[key];
  2509. if (val?.length) {
  2510. this.setMovieResource({ [key]: val });
  2511. continue;
  2512. }
  2513.  
  2514. needs.push(key);
  2515. }
  2516. if (!needs.length) return;
  2517.  
  2518. if (needs.includes("img")) {
  2519. DOC.head.insertAdjacentHTML("beforeend", '<meta name="referrer" content="never">');
  2520. let _code = code;
  2521. if (/^HEYZO-\d/i.test(_code)) _code = _code.replace("-", " ");
  2522.  
  2523. Apis.fetchJavStore(_code).then(res => res.img && this.setMovieResource(res));
  2524. Apis.fetchBlogJav(_code).then(res => this.setMovieResource(res));
  2525. }
  2526. if (needs.includes("video")) {
  2527. Apis.fetchVideoByGuess({ code, isVR, cid }).then(res => res.video && this.setMovieResource(res));
  2528. Apis.fetchVideoByStudio({ code, studio }).then(res => res.video && this.setMovieResource(res));
  2529. Apis.fetchJavSpyl(code).then(res => this.setMovieResource(res));
  2530. Apis.fetchAVPreview(code).then(res => this.setMovieResource(res));
  2531. if (FC2_REGEX.test(code)) Apis.fetchFC2(code).then(res => this.setMovieResource(res));
  2532. }
  2533. if (needs.includes("player")) Apis.fetchNetflav(code).then(res => this.setMovieResource(res));
  2534. if (needs.includes("title")) Apis.fetchTranslate(title).then(res => this.setMovieResource(res));
  2535. if (needs.includes("suk_magnet")) Apis.fetchSukebei(code).then(res => this.setMovieResource(res));
  2536. if (needs.includes("bts_magnet")) Apis.fetchBTSOW(code).then(res => this.setMovieResource(res));
  2537. if (needs.includes("btd_magnet")) {
  2538. Apis.fetchBTDigg(code).then(res => {
  2539. if (res.btd_magnet.length) return this.setMovieResource(res);
  2540. Apis.fetchBTDigg(code, 1).then(re => this.setMovieResource(re));
  2541. });
  2542. }
  2543.  
  2544. const isMGS = ["mgs_score", "video"];
  2545. if (needs.some(item => isMGS.includes(item))) {
  2546. Apis.fetchMGS({ code, title }).then(res => this.setMovieResource(res));
  2547. }
  2548.  
  2549. const isLib = ["lib_score", "star"];
  2550. if (needs.some(item => isLib.includes(item))) Apis.fetchLib(code).then(res => this.setMovieResource(res));
  2551.  
  2552. const isDMM = ["dmm_score", "star", "video"];
  2553. if (needs.some(item => isDMM.includes(item))) {
  2554. Apis.fetchDMM({ code, title }).then(({ fetchVideo, fetchInfo }) => {
  2555. if (!fetchVideo) return this.setMovieResource({ dmm_score: [], star: [], video: "" });
  2556. if (needs.includes("video")) fetchVideo().then(re => this.setMovieResource(re));
  2557. if (needs.includes("dmm_score")) fetchInfo().then(re => this.setMovieResource(re));
  2558. });
  2559. }
  2560.  
  2561. if (Domain === "JavDB") return;
  2562. const isDB = ["db_score", "star", "video", "sub"];
  2563. if (needs.some(item => isDB.includes(item))) Apis.fetchDB(code).then(res => this.setMovieResource(res));
  2564. };
  2565. setMovieResource = function (res) {
  2566. if (!res) return;
  2567. const { code } = this.info;
  2568. const detail = Store.getDetail(code);
  2569. for (const [key, val] of Object.entries(res)) {
  2570. if (detail[key]?.length) continue;
  2571. if (val?.length) Store.upDetail(code, { [key]: val });
  2572. if (this.params.hasOwnProperty(key)) this.params[key] = val;
  2573. }
  2574. };
  2575. upMovieResource = function (key, update, start) {
  2576. if (!this.params.hasOwnProperty(key)) return;
  2577.  
  2578. start?.();
  2579. if (this.params[key]?.length) return update(this.params[key]);
  2580. Object.defineProperty(this.params, key, { set: update, get: undefined });
  2581. };
  2582. // M_JUMP
  2583. movieJump = ({ code }, start, update) => {
  2584. if (!code || !start || !update) return;
  2585.  
  2586. let jump = this.M_JUMP?.trim().split("\n\n").filter(Boolean);
  2587. let { length } = jump;
  2588. if (!length) return;
  2589.  
  2590. const regex = /^\[.+\]$/;
  2591. const replace = /\[|\]/g;
  2592. const urlReg = /^https?:\/\/[a-z0-9]+/i;
  2593.  
  2594. const group = [];
  2595. for (let index = 0; index < length; index++) {
  2596. const items = unique(jump[index].split("\n").filter(Boolean));
  2597. if (items.length <= 1 || !regex.test(items[0])) continue;
  2598.  
  2599. const list = items.filter(item => urlReg.test(item));
  2600. if (!list.length) continue;
  2601.  
  2602. group.push({
  2603. group: items[0].replace(replace, ""),
  2604. list: list.map(item => {
  2605. return {
  2606. url: `${item.replaceAll(REP, code).trim()}#${code}`,
  2607. query: item.includes("#query"),
  2608. name: item.split("//")[1].split("/")[0].split(".").at(-2),
  2609. };
  2610. }),
  2611. });
  2612. }
  2613. if (group.length) start(group);
  2614.  
  2615. const nodeList = DOC.querySelectorAll(".x-jump");
  2616. length = nodeList.length;
  2617. if (!length) return;
  2618.  
  2619. jump = Store.getDetail(code)?.jump ?? [];
  2620. for (let index = 0; index < length; index++) {
  2621. const node = nodeList[index];
  2622. const { query, href } = node.dataset;
  2623. if (query !== "true") continue;
  2624.  
  2625. const key = href.split("#")[0];
  2626. let item = jump.find(item => item.key.toLowerCase() === key.toLowerCase());
  2627. if (item) {
  2628. if (item.href) update(item, node);
  2629. continue;
  2630. }
  2631. request(key).then(dom => {
  2632. if (!dom) return;
  2633.  
  2634. item = captureQuery(code, dom);
  2635. if (!item?.href) return;
  2636.  
  2637. update(item, node);
  2638. jump.push({ ...item, key });
  2639. Store.upDetail(code, { jump });
  2640. });
  2641. }
  2642. };
  2643. // M_SORT
  2644. movieSort = (magnets, start) => {
  2645. if (!this.M_SORT) return magnets;
  2646.  
  2647. start?.();
  2648. magnets = magnets.filter(item => item.bytes > 300 * 1024 ** 2);
  2649. return magnets.length <= 1
  2650. ? magnets
  2651. : magnets.sort((pre, next) => {
  2652. if (pre.zh === next.zh) {
  2653. if (pre.bytes === next.bytes) return next.date - pre.date;
  2654. return next.bytes - pre.bytes;
  2655. } else {
  2656. return pre.zh > next.zh ? -1 : 1;
  2657. }
  2658. });
  2659. };
  2660.  
  2661. // D_MATCH
  2662. driveMatchForList = async (items, selectors) => {
  2663. if (!this.D_MATCH) return;
  2664.  
  2665. const update = ({ item }, res) => {
  2666. const node = item.querySelector(selectors);
  2667. if (!node) return;
  2668.  
  2669. const keys = ["n", "pc", "fid", "cid"];
  2670. const { classList, dataset } = node;
  2671.  
  2672. if (!res.length) {
  2673. keys.forEach(key => delete dataset[key]);
  2674. node.removeAttribute("title");
  2675. return classList.remove("x-zh", "x-player");
  2676. }
  2677.  
  2678. classList.add("x-player");
  2679. let str = "已有";
  2680. let _item = res[0];
  2681. const zh = res.find(({ n }) => ZH_REGEX.test(n));
  2682. if (zh) {
  2683. classList.add("x-zh");
  2684. str = "字幕";
  2685. _item = zh;
  2686. }
  2687. node.setAttribute("title", `${str}资源`);
  2688. keys.forEach(key => {
  2689. dataset[key] = _item[key];
  2690. });
  2691. };
  2692.  
  2693. const params = {};
  2694. for (let index = 0, { length } = items; index < length; index++) {
  2695. const item = items[index];
  2696. let { code } = item;
  2697. code = /^FC2-\d/i.test(code) ? code.replace("-", "-PPV-") : code;
  2698. item.code = code;
  2699.  
  2700. const { res } = Store.getDetail(code);
  2701. if (res) {
  2702. update(item, res);
  2703. continue;
  2704. }
  2705.  
  2706. const key = code.split("-")[0];
  2707. params.hasOwnProperty(key) ? params[key].push(item) : (params[key] = [item]);
  2708. }
  2709. for (const [prefix, items] of Object.entries(params)) {
  2710. let res = Store.getResource(prefix);
  2711. if (!res) {
  2712. res = await Apis._driveSearch(prefix);
  2713. if (!res) break;
  2714. Store.upResource(prefix, res);
  2715. }
  2716. if (!res.length) continue;
  2717.  
  2718. for (let index = 0, { length } = items; index < length; index++) {
  2719. const item = items[index];
  2720. const { regex } = codeParse(item.code);
  2721. const _res = res.filter(({ n }) => regex.test(n));
  2722. if (_res.length) update(item, _res);
  2723. }
  2724. }
  2725. };
  2726. driveMatchForMovie = async function ({ code }, update, start) {
  2727. if (!this.D_MATCH) return;
  2728.  
  2729. start?.();
  2730. const cid = await this.driveCid();
  2731. if (cid === undefined) return update([]);
  2732.  
  2733. const { prefix, regex } = codeParse(code);
  2734. const list = await Promise.allSettled([Apis._driveSearch(prefix, ["t"]), Apis.driveVideo(cid)]);
  2735.  
  2736. let res;
  2737. for (let index = 0, { length } = list; index < length; index++) {
  2738. const { status, value } = list[index];
  2739. if (status !== "fulfilled" || !value?.length) continue;
  2740.  
  2741. if (!res) res = [];
  2742. for (let idx = 0, len = value.length; idx < len; idx++) {
  2743. const item = value[idx];
  2744. if (res.some(re => re.fid === item.fid) || !regex.test(item.n)) continue;
  2745. res.push(item);
  2746. }
  2747. }
  2748. update(res || []);
  2749. if (res) Store.upDetail(code, { res });
  2750. };
  2751. // D_LOL
  2752. upLOLTarget = () => {
  2753. if (!this.D_MATCH || !this.D_LOL) return;
  2754. GM_addStyle('.x-btn{display:revert}.x-btn::after{content:"离线"}');
  2755. };
  2756. driveListOffLine = async (e, { code, title }) => {
  2757. e.preventDefault();
  2758. e.stopPropagation();
  2759.  
  2760. const LOCK = "active";
  2761. const { classList } = e.target;
  2762. if (classList.contains(LOCK)) return;
  2763. classList.add(LOCK);
  2764.  
  2765. const magnet = paramParse(this.M_MAGNET, ["suk", "bts", "btd"]);
  2766. if (!magnet.length) return classList.remove(LOCK);
  2767.  
  2768. const detail = Store.getDetail(code);
  2769. let magnets = magnet.map(item => detail[`${item}_magnet`] ?? []).flat();
  2770. if (!magnets.length) {
  2771. const list = {
  2772. suk: () => Apis.fetchSukebei(code),
  2773. bts: () => Apis.fetchBTSOW(code),
  2774. btd: () => Apis.fetchBTDigg(code),
  2775. };
  2776.  
  2777. const res = await Promise.allSettled(magnet.map(item => list[item]()));
  2778. for (let index = 0, { length } = res; index < length; index++) {
  2779. const { status, value } = res[index];
  2780. if (status !== "fulfilled" || !value) continue;
  2781.  
  2782. Store.upDetail(code, value);
  2783. magnets.push(...value[`${magnet[index]}_magnet`]);
  2784. }
  2785. }
  2786. if (!magnets.length) return classList.remove(LOCK);
  2787.  
  2788. magnets = this.movieSort(magnets);
  2789. magnets = this.driveFail(magnets);
  2790.  
  2791. if (/^\$\{.+\}$/.test(this.D_CID)) this.D_CID = "";
  2792. const [wp_path_id, sign] = await Promise.all([this.driveCid("create"), Apis.driveSign()]);
  2793. if (!wp_path_id || !sign) return classList.remove(LOCK);
  2794.  
  2795. for (let index = 0, { length } = magnets; index < length; index++) {
  2796. const isLast = index + 1 === length;
  2797. const { link: url, zh } = magnets[index];
  2798.  
  2799. let res = await Apis.driveAddTask({ url, wp_path_id, ...sign });
  2800. if (!res) break;
  2801.  
  2802. const { state, errcode, error_msg } = res;
  2803. if (!state) {
  2804. if ([10008, 10007].includes(errcode)) {
  2805. if (errcode === 10008 && !isLast) continue;
  2806. notify({ type: "warn", title: error_msg });
  2807. break;
  2808. }
  2809. if (errcode === 911) {
  2810. classList.add("pending");
  2811.  
  2812. if (Store.getVerifyStatus() !== "pending") {
  2813. notify({ type: "warn", title: `${code} 任务暂停,等待校验` });
  2814. verify();
  2815. }
  2816. const listener = GM_addValueChangeListener(
  2817. "VERIFY_STATUS",
  2818. (name, old_value, new_value, remote) => {
  2819. if (!remote || !["verified", "failed"].includes(new_value)) return;
  2820. GM_removeValueChangeListener(listener);
  2821. classList.remove(LOCK, "pending");
  2822. if (new_value === "verified") this.driveListOffLine(e, { code, title });
  2823. }
  2824. );
  2825. break;
  2826. }
  2827. }
  2828.  
  2829. const cid = wp_path_id;
  2830. const clickUrl = FILE_RUL.replace(REP, cid);
  2831. res = await this.driveVerify({ code, cid });
  2832. if (!res) {
  2833. if (!isLast) continue;
  2834. notify({
  2835. type: "info",
  2836. title: `${code} 验证失败`,
  2837. clickUrl: "https://115.com/?tab=offline&mode=wangpan",
  2838. });
  2839. break;
  2840. }
  2841. if (res?.length) {
  2842. notify({
  2843. type: "success",
  2844. title: `${code} 离线成功`,
  2845. text: `点击跳转目录`,
  2846. clickUrl: FILE_RUL.replace(REP, res[0].cid),
  2847. });
  2848.  
  2849. const list = [() => this.driveClear({ cid, res, code })];
  2850. let { file_name, fetch } = this.driveRename({ cid, res, zh, code, title });
  2851. if (fetch) {
  2852. res[0].n = file_name;
  2853. list.push(() => fetch());
  2854. }
  2855.  
  2856. Store.upDetail(code, { res });
  2857. const { selectors, upQuery } = this.click;
  2858. const item = e.target.closest(selectors);
  2859. if (item) this.driveMatchForList([{ item, code }], upQuery);
  2860.  
  2861. await Promise.allSettled(list.map(item => item()));
  2862. } else {
  2863. notify({ type: "info", title: `${code} 任务结束`, clickUrl });
  2864. }
  2865. break;
  2866. }
  2867. if (!classList.contains("pending")) classList.remove(LOCK);
  2868. };
  2869. // D_CID
  2870. driveCid = async function (type = "find") {
  2871. let cid = this.D_CID.trim() || "${云下载}";
  2872.  
  2873. if (/^\$\{.+\}$/.test(cid)) {
  2874. let isFirst = true;
  2875. const arr = cid
  2876. .replace(/\$|\{|\}/g, "")
  2877. .split("/")
  2878. .map(item => item.trim())
  2879. .filter(Boolean);
  2880.  
  2881. for (let index = 0, { length } = arr; index < length; index++) {
  2882. let item = arr[index];
  2883. if (!item.startsWith("#")) {
  2884. cid = item;
  2885. break;
  2886. }
  2887.  
  2888. item = item.replace(/^#/, "");
  2889. if (!item) continue;
  2890.  
  2891. cid = this.info?.[item];
  2892. if (!cid?.length) {
  2893. isFirst = false;
  2894. continue;
  2895. }
  2896.  
  2897. if (typeof cid !== "string") cid = cid[0];
  2898. break;
  2899. }
  2900. if (!cid?.length) cid = "云下载";
  2901.  
  2902. cid = await Apis._driveCid(cid, type);
  2903. if (cid && (isFirst || type !== "find")) this.D_CID = cid;
  2904. }
  2905.  
  2906. return cid;
  2907. };
  2908. // D_VERIFY
  2909. driveVerify = async ({ code, cid }) => {
  2910. let verify = this.D_VERIFY <= 0;
  2911.  
  2912. const { regex } = codeParse(code);
  2913. const exist = Store.getDetail(code)?.res ?? [];
  2914. for (let idx = 0; idx < this.D_VERIFY; idx++) {
  2915. await delay();
  2916.  
  2917. let res = await Apis.driveVideo(cid, ["ico"]);
  2918. res = res.filter(({ n, t }) => regex.test(n) && t.startsWith(getDate()));
  2919. if (!res?.length) continue;
  2920.  
  2921. res = res.filter(item => !exist.find(e => e.fid === item.fid));
  2922. if (!res.length) continue;
  2923.  
  2924. verify = res;
  2925. break;
  2926. }
  2927. return verify;
  2928. };
  2929. // D_FAIL
  2930. driveFail = magnets => {
  2931. const num = parseInt(this.D_FAIL ?? 0, 10);
  2932. return num <= 0 ? magnets : magnets.slice(0, num);
  2933. };
  2934. // D_CLEAR
  2935. driveClear = ({ cid, res, code }) => {
  2936. if (!this.D_CLEAR) return;
  2937.  
  2938. const { regex } = codeParse(code);
  2939. unique(res.map(item => item.cid).filter(item => item !== cid)).forEach(async pid => {
  2940. let fids = await Apis.driveFile(pid);
  2941. if (!fids?.data) return;
  2942.  
  2943. fids = fids.data.filter(({ n }) => !regex.test(n));
  2944. if (fids.length) Apis.driveClear({ pid, fids });
  2945. });
  2946. };
  2947. // D_RENAME
  2948. driveRename = ({ cid, res, zh, code, title }) => {
  2949. let file_name = this.D_RENAME?.trim();
  2950. if (!file_name) return { file_name: `${code} ${title}` };
  2951.  
  2952. file_name = file_name
  2953. .replace(/\$\{字幕\}/g, zh ? "【中文字幕】" : "")
  2954. .replace(/\$\{番号\}/g, code)
  2955. .replace(/\$\{标题\}/g, title);
  2956.  
  2957. if (!codeParse(code).regex.test(file_name)) file_name = `${code} - ${file_name}`;
  2958.  
  2959. res = res.filter(item => item.ico);
  2960. const noRegex = /\$\{序号\}/g;
  2961. const data = [];
  2962.  
  2963. unique(res.map(item => `${item.cid}/${item.ico}`)).forEach(key => {
  2964. const [_cid, _ico] = key.split("/");
  2965. res.filter(item => item.cid === _cid && item.ico === _ico).forEach((item, index) => {
  2966. data.push({ ...item, file_name: `${file_name.replace(noRegex, index + 1)}.${_ico}` });
  2967. });
  2968. });
  2969.  
  2970. file_name = file_name.replace(noRegex, "");
  2971. unique(res.map(item => item.cid).filter(item => item !== cid)).forEach(fid => {
  2972. data.push({ fid, file_name });
  2973. });
  2974.  
  2975. return { file_name, fetch: () => Apis.driveRename(data) };
  2976. };
  2977. // D_TAG
  2978. driveTag = async function (file_ids) {
  2979. if (!file_ids?.length || !this.D_TAG?.trim()) return;
  2980.  
  2981. let tag = paramParse(this.D_TAG, ["genre", "star"]);
  2982. if (!tag.length) return;
  2983.  
  2984. tag = tag.map(key => this.info[key] ?? []);
  2985. tag = tag
  2986. .flat()
  2987. .map(item => item.trim())
  2988. .filter(Boolean);
  2989. if (!tag.length) return;
  2990.  
  2991. const label = await Apis.driveLabel();
  2992. if (!label?.length) return;
  2993.  
  2994. let file_label = unique(tag).map(item => label.find(({ name }) => name === item)?.id ?? "");
  2995. file_label = file_label.filter(Boolean);
  2996. if (file_label.length) Apis.driveTag({ file_ids, file_label: file_label.join(",") });
  2997. };
  2998. // D_UPLOAD
  2999. driveUpload = async function ({ cid, name, res }) {
  3000. const keys = res;
  3001. res = res.map(key => this.info[key]).filter(Boolean);
  3002. const list = await Promise.allSettled(res.map(item => request(item, {}, "GET", { responseType: "blob" })));
  3003.  
  3004. const target = `U_1_${cid}`;
  3005. for (let index = 0, { length } = list; index < length; index++) {
  3006. const { status, value } = list[index];
  3007. if (status === "rejected" || !value?.size) continue;
  3008.  
  3009. const filename = `${name}_${keys[index]}_.${res[index].split(".").at(-1)}`;
  3010. const init = await Apis.driveInitUpload({ filename, filesize: value.size, target });
  3011. if (!init?.host) continue;
  3012.  
  3013. await Apis.driveUpload(init, value, filename);
  3014. }
  3015. notify({ text: "", type: "", title: "图传任务结束" });
  3016. };
  3017. // D_MODIFY
  3018. upModifyTarget = () => {
  3019. const modify = paramParse(this.D_MODIFY, ["clear", "rename", "tag", "upload", "delete"]);
  3020. if (!modify.length) return modify;
  3021.  
  3022. GM_addStyle(".x-btn{display:revert}");
  3023. if (!modify.includes("delete")) return modify;
  3024.  
  3025. GM_addStyle(".x-btn{background:var(--x-red)}.x-btn::after{content:'删除'}");
  3026. return modify;
  3027. };
  3028. driveModify = async function (e, modify) {
  3029. e.preventDefault();
  3030. e.stopPropagation();
  3031.  
  3032. const { classList, parentNode } = e.target;
  3033. if (classList.contains("active")) return;
  3034.  
  3035. classList.add("active");
  3036. const { cid, fid, n } = parentNode.dataset;
  3037. const list = [];
  3038.  
  3039. if (modify.includes("delete")) {
  3040. list.push(() => Apis.driveClear({ pid: cid, fids: [{ fid }] }));
  3041. } else {
  3042. const { code, title } = this.info;
  3043. const { regex } = codeParse(code);
  3044.  
  3045. let file_name = this.D_RENAME?.trim();
  3046. if (file_name) {
  3047. file_name = file_name
  3048. .replace(/\$\{字幕\}/g, ZH_REGEX.test(n) ? "【中文字幕】" : "")
  3049. .replace(/\$\{番号\}/g, code)
  3050. .replace(/\$\{标题\}/g, title)
  3051. .replace(/\$\{序号\}/g, "");
  3052. if (!regex.test(file_name)) file_name = `${code} - ${file_name}`;
  3053. }
  3054.  
  3055. if (modify.includes("rename") && file_name) {
  3056. let name = n.split(".");
  3057. name.pop();
  3058. if (name.join("") !== file_name) list.push(() => Apis.driveRename([{ fid, file_name }]));
  3059. }
  3060. if (modify.includes("tag")) list.push(() => this.driveTag(fid));
  3061. if (modify.some(item => ["clear", "upload"].includes(item))) {
  3062. const { data } = await Apis.driveFile(cid);
  3063.  
  3064. if (modify.includes("clear")) {
  3065. const fids = data.filter(({ n }) => !regex.test(n));
  3066. if (fids.length) list.push(() => Apis.driveClear({ pid: cid, fids }));
  3067. }
  3068. if (modify.includes("upload")) {
  3069. let uploads = paramParse(this.D_UPLOAD, ["cover", "img"]);
  3070. if (uploads.length) {
  3071. const items = data.filter(item => regex.test(item.n) && item.class === "PIC");
  3072. uploads = uploads.filter(item => !items.some(({ n }) => n.includes(`_${item}_`)));
  3073. if (uploads.length) {
  3074. file_name = file_name || `${code} ${title}`;
  3075. list.push(() => this.driveUpload({ cid, name: file_name, res: uploads }));
  3076. }
  3077. }
  3078. }
  3079. }
  3080. }
  3081.  
  3082. if (list.length) await Promise.allSettled(list.map(item => item()));
  3083. classList.remove("active");
  3084. if (!DOC.querySelector(".x-btn.active")) this.driveMatch();
  3085. };
  3086. // OFFLINE
  3087. driveOffLine = async function (e, { code, title, magnets }) {
  3088. const { target } = e;
  3089. const { magnet: type } = target.dataset;
  3090. if (!type) return;
  3091.  
  3092. e.preventDefault();
  3093. e.stopPropagation();
  3094.  
  3095. const setTab = type === "all";
  3096. magnets = setTab ? this.driveFail(magnets) : magnets.filter(item => item.link === type);
  3097. const length = magnets?.length ?? 0;
  3098. if (!length) return;
  3099.  
  3100. const { classList } = target;
  3101. classList.remove("pending");
  3102. classList.add("active");
  3103. const originText = setTab ? "一键离线" : "添加离线";
  3104. target.textContent = "请求中...";
  3105. target.inert = true;
  3106.  
  3107. const handleComplete = () => {
  3108. classList.remove("active");
  3109. target.textContent = originText;
  3110. target.inert = false;
  3111. };
  3112.  
  3113. const [wp_path_id, sign] = await Promise.all([this.driveCid("create"), Apis.driveSign()]);
  3114. if (!wp_path_id || !sign) return handleComplete();
  3115.  
  3116. const warnTitle = `${code} 一键离线失败`;
  3117. if (setTab) setTabBar({ title: `${code} 一键离线中...` });
  3118. for (let index = 0; index < length; index++) {
  3119. const isLast = index + 1 === length;
  3120. const { link: url, zh } = magnets[index];
  3121.  
  3122. let res = await Apis.driveAddTask({ url, wp_path_id, ...sign });
  3123. if (!res) {
  3124. if (!index) return handleComplete();
  3125. break;
  3126. }
  3127.  
  3128. const { state, errcode, error_msg } = res;
  3129. if (!state) {
  3130. if ([10008, 10007].includes(errcode)) {
  3131. if (errcode === 10008 && !isLast) continue;
  3132. notify({ type: "warn", title: error_msg });
  3133. if (setTab) setTabBar({ type: "warn", title: warnTitle });
  3134. break;
  3135. }
  3136. if (errcode === 911) {
  3137. classList.add("pending");
  3138. target.textContent = "校验中...";
  3139. const msg = { type: "warn", title: `${code} 任务暂停,等待校验` };
  3140.  
  3141. if (setTab) setTabBar(msg);
  3142. if (Store.getVerifyStatus() !== "pending") {
  3143. notify(msg);
  3144. verify();
  3145. }
  3146. const listener = GM_addValueChangeListener(
  3147. "VERIFY_STATUS",
  3148. (name, old_value, new_value, remote) => {
  3149. if (!remote || !["verified", "failed"].includes(new_value)) return;
  3150. GM_removeValueChangeListener(listener);
  3151. if (new_value !== "verified") return handleComplete();
  3152. this.driveOffLine(e, { magnets: magnets.slice(index), code, title });
  3153. }
  3154. );
  3155. break;
  3156. }
  3157. }
  3158.  
  3159. const cid = wp_path_id;
  3160. const clickUrl = FILE_RUL.replace(REP, cid);
  3161. res = await this.driveVerify({ code, cid });
  3162. if (!res) {
  3163. if (!isLast) continue;
  3164. notify({
  3165. type: "info",
  3166. title: `${code} 验证失败`,
  3167. clickUrl: "https://115.com/?tab=offline&mode=wangpan",
  3168. setTab,
  3169. });
  3170. break;
  3171. }
  3172. if (res?.length) {
  3173. const _cid = res[0].cid;
  3174. const uploads = paramParse(this.D_UPLOAD, ["cover", "img"]);
  3175. notify({
  3176. type: "success",
  3177. title: `${code} 离线成功`,
  3178. setTab,
  3179. text: `点击跳转目录${uploads.length ? ",尝试图传中..." : ""}`,
  3180. clickUrl: FILE_RUL.replace(REP, _cid),
  3181. });
  3182. const { file_name, fetch } = this.driveRename({ cid, res, zh, code, title });
  3183. if (fetch) await fetch();
  3184.  
  3185. if (uploads.length) this.driveUpload({ cid: _cid, name: file_name, res: uploads });
  3186. this.driveTag(
  3187. res
  3188. .map(({ fid }) => fid)
  3189. .filter(Boolean)
  3190. .join(",")
  3191. );
  3192. this.driveClear({ cid, res, code });
  3193. } else {
  3194. notify({ type: "info", title: `${code} 任务结束`, clickUrl, setTab });
  3195. }
  3196. break;
  3197. }
  3198.  
  3199. if (classList.contains("pending")) return;
  3200. await delay();
  3201. this.driveMatch();
  3202. handleComplete();
  3203. };
  3204.  
  3205. // A_LINK
  3206. advancedLink = async (e, { type = "left", defaultLink }) => {
  3207. e.preventDefault();
  3208. e.stopPropagation();
  3209.  
  3210. const { n: name, pc: pickcode, fid, cid } = e.target.dataset;
  3211. defaultLink = defaultLink ?? type === "left" ? `${PICK_RUL}${pickcode}` : FILE_RUL.replace(REP, cid);
  3212.  
  3213. if (!fid || fid === "undefined") return openInTab(defaultLink);
  3214.  
  3215. let config = this.A_LINK?.trim()?.split("\n");
  3216. let index = config.findIndex(item => item === `[${type}]`);
  3217. if (index === -1) return openInTab(defaultLink);
  3218.  
  3219. config = config[index + 1]?.trim();
  3220. if (!config) return openInTab(defaultLink);
  3221.  
  3222. config = config
  3223. .replaceAll("${#pickcode}", pickcode)
  3224. .replaceAll("${#cid}", cid)
  3225. .replaceAll("${#name}", encodeURIComponent(name));
  3226. if (!config.includes("${#path}")) return openInTab(config);
  3227.  
  3228. let path = await Apis.driveDetail(fid);
  3229. if (!path?.paths?.length) return;
  3230.  
  3231. path = path.paths
  3232. .map(item => item.file_name)
  3233. .splice(1)
  3234. .join("/");
  3235. openInTab(config.replaceAll("${#path}", encodeURIComponent(path)));
  3236. };
  3237. }
  3238.  
  3239. class JavDB extends Common {
  3240. constructor() {
  3241. super();
  3242. return super.init();
  3243. }
  3244.  
  3245. excludeMenu = ["G_DARK", "L_MIT", "M_SUB"];
  3246. routes = {
  3247. list: /^\/($|guess|(un)?censored|western|fc2|anime|(advanced_)?search|video_codes|tags|rankings|actors|series|makers|directors|publishers|lists|users)/i,
  3248. movie: /^\/v\/\w+/i,
  3249. others: /.*/i,
  3250. };
  3251. _style = {
  3252. common: `
  3253. html {
  3254. overflow: overlay;
  3255. padding-bottom: 0 !important;
  3256. }
  3257. body {
  3258. min-height: 100%;
  3259. }
  3260. .section {
  3261. padding: 20px;
  3262. }
  3263. #search-bar-container {
  3264. margin-bottom: 8px !important;
  3265. }
  3266. .search-panel button {
  3267. border: none;
  3268. }
  3269. #search-type, #video-search {
  3270. z-index: auto;
  3271. border: none;
  3272. box-shadow: none;
  3273. }
  3274. .title:not(:last-child) {
  3275. margin-bottom: 20px;
  3276. }
  3277. .main-title {
  3278. padding-top: 0;
  3279. }
  3280. .app-desktop-banner, #footer {
  3281. display: none !important;
  3282. }
  3283. :root[data-theme="dark"] ::-webkit-scrollbar-thumb {
  3284. background: var(--x-grey) !important;
  3285. }
  3286. a.box:is(:focus, :hover) {
  3287. box-shadow: none !important;
  3288. }
  3289. :root[data-theme="dark"] .box:hover {
  3290. background: unset;
  3291. }
  3292. :root[data-theme="dark"] img {
  3293. filter: brightness(.9) contrast(.9) !important;
  3294. }
  3295. nav.pagination {
  3296. display: none;
  3297. margin: 0 -4px 20px !important;
  3298. padding: 0;
  3299. border: none;
  3300. }
  3301. :root[data-theme="dark"] nav.pagination {
  3302. border: none !important;
  3303. }
  3304. .float-buttons {
  3305. right: 8px;
  3306. }
  3307. `,
  3308. };
  3309. search = {
  3310. selectors: "#video-search",
  3311. pathname: `/search?q=${REP}&f=all`,
  3312. };
  3313. click = {
  3314. selectors: [":is(.movie-list, .actors, .section-container) a:has(strong)"],
  3315. codeQuery: ".video-title strong",
  3316. upQuery: ".cover",
  3317. };
  3318. merge = {
  3319. start: list => {
  3320. DOC.querySelector("#navbar-menu-hero .navbar-start")?.insertAdjacentHTML(
  3321. "beforeend",
  3322. `<div class="navbar-item has-dropdown is-hoverable">
  3323. <a class="navbar-link" href="/?merge=${list[0]}">合并列表</a>
  3324. <div class="navbar-dropdown is-boxed">
  3325. ${list.map(item => `<a class="navbar-item" href="/?merge=${item}">${item}</a>`).join("")}
  3326. </div>
  3327. </div>`
  3328. );
  3329. },
  3330. };
  3331.  
  3332. list = {
  3333. docStart() {
  3334. const style = `
  3335. .awards,
  3336. .form-panel .user-profile,
  3337. .section:has(.actors, .movie-list, .section-container, .user-container .common-list) {
  3338. padding-bottom: 0;
  3339. }
  3340. :is(.tabs, .message):not(:last-child) {
  3341. margin-bottom: 20px;
  3342. }
  3343. .movie-list {
  3344. opacity: 0;
  3345. }
  3346. .actors, .movie-list, .section-container {
  3347. gap: 20px !important;
  3348. margin: 0 0 20px !important;
  3349. padding-bottom: 0;
  3350. }
  3351. .container > br,
  3352. .actor-filter hr,
  3353. .movie-list:has(.tags),
  3354. .user-container > .column.is-10 > .message.is-warning {
  3355. display: none;
  3356. }
  3357. .main-tabs, .box:has(> form) {
  3358. margin-bottom: 20px !important;
  3359. }
  3360. .toolbar {
  3361. margin-top: -10px;
  3362. padding-bottom: 0;
  3363. word-spacing: -20px;
  3364. }
  3365. .toolbar * {
  3366. word-spacing: 0;
  3367. }
  3368. .toolbar .button-group, .actor-tags .content .tag {
  3369. margin: 0 10px 10px 0;
  3370. }
  3371. #t {
  3372. height: 2.2rem;
  3373. }
  3374. #tags {
  3375. margin: -20px 0 20px;
  3376. }
  3377. #tags dt a.tag-expand {
  3378. margin-top: .44rem;
  3379. }
  3380. .section:has(> .awards, > .user-container) {
  3381. padding: 0;
  3382. }
  3383. .divider-title {
  3384. margin-bottom: 16px !important;
  3385. padding-bottom: 0;
  3386. border-bottom: none;
  3387. }
  3388. .actors .box {
  3389. margin-bottom: 0;
  3390. }
  3391. .actor-filter-toolbar {
  3392. padding-bottom: 20px;
  3393. }
  3394. .section-columns {
  3395. margin-bottom: 8px !important;
  3396. padding-top: 0;
  3397. }
  3398. .columns:has(> .section-addition) {
  3399. margin-bottom: 8px;
  3400. }
  3401. .actor-tags {
  3402. margin-bottom: 20px !important;
  3403. padding-bottom: 0;
  3404. font-size: 0;
  3405. border-bottom: none;
  3406. }
  3407. .actor-tags .content:not(.collapse) {
  3408. margin-bottom: -10px;
  3409. }
  3410. .user-container {
  3411. margin: -10px -10px 0 !important;
  3412. }
  3413. .user-container > .column {
  3414. padding: 10px 10px 0;
  3415. }
  3416. .user-container > .column:first-child {
  3417. padding-bottom: 10px;
  3418. }
  3419. .user-container .common-list ul {
  3420. margin-bottom: 20px;
  3421. }
  3422. .list-item:not(:last-child) {
  3423. border-bottom: 1px solid #dbdbdb;
  3424. }
  3425. .user-container .common-list .list-item {
  3426. align-items: center;
  3427. margin: 0;
  3428. padding: 10px 2px;
  3429. }
  3430. .user-container .common-list .list-item:first-child {
  3431. padding-top: 0;
  3432. }
  3433. .user-container .common-list .list-item:last-child {
  3434. padding-bottom: 0;
  3435. }
  3436. .user-container .common-list .list-item .column {
  3437. padding: 0;
  3438. }
  3439. .user-container .common-list .list-item .column:first-child {
  3440. flex: 1;
  3441. }
  3442. .user-container .common-list .list-item .column:last-child {
  3443. width: auto;
  3444. }
  3445. `;
  3446. const layout = `
  3447. @media (width < 576px) {
  3448. .movie-list.v, .actors {
  3449. grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  3450. }
  3451. .movie-list.h {
  3452. grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
  3453. }
  3454. }
  3455. @media (width >= 576px) {
  3456. .movie-list.v, .actors {
  3457. grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  3458. }
  3459. .movie-list.h {
  3460. grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  3461. }
  3462. }
  3463. @media (width >= 768px) {
  3464. .movie-list.v, .actors {
  3465. grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  3466. }
  3467. .movie-list.h {
  3468. grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  3469. }
  3470. }
  3471. @media (width >= 992px) {
  3472. .movie-list.v, .actors {
  3473. grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
  3474. }
  3475. .movie-list.h {
  3476. grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  3477. }
  3478. }
  3479. @media (width >= 1200px) {
  3480. .movie-list.v, .actors {
  3481. grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
  3482. }
  3483. .movie-list.h {
  3484. grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  3485. }
  3486. }
  3487. @media (width >= 1400px) {
  3488. .movie-list.v, .actors {
  3489. grid-template-columns: repeat(6, minmax(0, 1fr)) !important;
  3490. }
  3491. .movie-list.h {
  3492. grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  3493. }
  3494. }
  3495. .user-container .column:has(.movie-list, .actors) {
  3496. container-type: inline-size;
  3497. }
  3498. @container (width < 576px) {
  3499. .column .movie-list {
  3500. grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
  3501. }
  3502. .column .movie-list.v, .actors {
  3503. grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  3504. }
  3505. }
  3506. @container (width >= 576px) {
  3507. .column .movie-list {
  3508. grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  3509. }
  3510. .column .movie-list.v, .actors {
  3511. grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  3512. }
  3513. }
  3514. @container (width >= 768px) {
  3515. .column .movie-list {
  3516. grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  3517. }
  3518. .column .movie-list.v, .actors {
  3519. grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  3520. }
  3521. }
  3522. @container (width >= 992px) {
  3523. .column .movie-list {
  3524. grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  3525. }
  3526. .column .movie-list.v, .actors {
  3527. grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
  3528. }
  3529. }
  3530. @container (width >= 1200px) {
  3531. .column .movie-list {
  3532. grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  3533. }
  3534. .column .movie-list.v, .actors {
  3535. grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
  3536. }
  3537. }
  3538. @container (width >= 1400px) {
  3539. .column .movie-list {
  3540. grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  3541. }
  3542. .column .movie-list.v, .actors {
  3543. grid-template-columns: repeat(6, minmax(0, 1fr)) !important;
  3544. }
  3545. }
  3546. `;
  3547. const card = `
  3548. :is(.movie-list, .actors) .box {
  3549. padding: 0 0 10px;
  3550. }
  3551. .movie-list .item .cover, .actor-box a figure {
  3552. background: var(--x-sub-ftc);
  3553. }
  3554. :root[data-theme="dark"] :is(.movie-list .item .cover, .actor-box a figure) {
  3555. background: var(--x-grey);
  3556. }
  3557. .movie-list .item .cover {
  3558. padding: 0 !important;
  3559. aspect-ratio: var(--x-cover-ratio);
  3560. }
  3561. .actors .box img {
  3562. height: 100%;
  3563. object-fit: cover;
  3564. opacity: 0;
  3565. }
  3566. .movie-list .item .cover img {
  3567. width: 100%;
  3568. object-fit: contain;
  3569. opacity: 0;
  3570. }
  3571. .movie-list .item .cover:hover img {
  3572. z-index: auto;
  3573. transform: none;
  3574. }
  3575. .movie-list.v .item .cover {
  3576. aspect-ratio: var(--x-thumb-ratio);
  3577. }
  3578. .movie-list.v .item .cover img {
  3579. object-fit: cover !important;
  3580. }
  3581. .movie-list .item .video-title, .actor-box a strong, .section-container .box {
  3582. font-size: 14px;
  3583. line-height: var(--x-line-h);
  3584. }
  3585. .movie-list .item .video-title {
  3586. box-sizing: content-box;
  3587. padding: 10px 0 0;
  3588. }
  3589. .movie-list .item .score {
  3590. padding: 0;
  3591. }
  3592. .movie-list .item .box :is(.video-title, .score, .tags, .meta-buttons), .actor-box a strong {
  3593. padding: 10px 10px 0;
  3594. }
  3595. .movie-list .item .meta {
  3596. padding: 0 10px;
  3597. }
  3598. .movie-list .box .tags {
  3599. min-height: 44px;
  3600. margin-bottom: -10px;
  3601. }
  3602. .movie-list .box .tags .tag {
  3603. margin-bottom: 10px;
  3604. }
  3605. .actor-box a figure {
  3606. aspect-ratio: var(--x-avatar-ratio);
  3607. }
  3608. .actor-box a.button.is-danger {
  3609. width: auto;
  3610. margin: 10px 10px 0 !important;
  3611. }
  3612. .movie-list .box .meta-buttons a.button.is-danger {
  3613. margin: 0 !important;
  3614. }
  3615. .section-container a:has(> strong) {
  3616. display: flex;
  3617. }
  3618. .section-container .box strong {
  3619. flex: 1;
  3620. padding-right: 10px;
  3621. overflow: hidden;
  3622. white-space: nowrap;
  3623. text-overflow: ellipsis;
  3624. }
  3625. `;
  3626. this.globalDark(`${this._style.common}${style}${layout}${card}${this.listMovieTitle()}`);
  3627. this.listMerge();
  3628. this.upLOLTarget();
  3629. },
  3630. contentLoaded() {
  3631. this.globalSearch();
  3632. if (location.pathname === "/users/favorite_lists") {
  3633. this.click.selectors.push(".common-list .column.is-10 a");
  3634. }
  3635. this.globalClick();
  3636. this.modifyLayout();
  3637. },
  3638. modifyLayout() {
  3639. const tags = DOC.querySelector("#tags:not(select)");
  3640. if (tags) {
  3641. const toggle = DOC.create("button", { class: "button is-info", type: "button" }, "折叠");
  3642. const tabs = DOC.querySelector(".tabs.is-boxed");
  3643. tabs.classList.add("is-align-items-flex-end");
  3644. tabs.append(toggle);
  3645. toggle.addEventListener("click", () => tags.classList.toggle("is-hidden"));
  3646. }
  3647.  
  3648. for (const list of DOC.querySelectorAll(".movie-list:not(:has(.tags))")) {
  3649. const _list = list.cloneNode(true);
  3650. const items = this.modifyItem([..._list.querySelectorAll(".item")]);
  3651. _list.innerHTML = "";
  3652. _list.append(...items);
  3653. _list.classList.add("x-in");
  3654. list.replaceWith(_list);
  3655. }
  3656.  
  3657. let container;
  3658. const lists = DOC.querySelectorAll(".actors");
  3659. const { length } = lists;
  3660. if (length) {
  3661. if (length === 1) {
  3662. container = lists[0];
  3663. } else {
  3664. for (let index = 0; index < length; index++) {
  3665. for (const item of lists[index].querySelectorAll(".box")) fadeInImg(item);
  3666. }
  3667. }
  3668. }
  3669. container =
  3670. container ??
  3671. DOC.querySelector([
  3672. ".movie-list:has(.tags)",
  3673. ".section-container:has(.box)",
  3674. ".user-container:has(nav.pagination) .common-list ul",
  3675. ]);
  3676. if (!container) {
  3677. if (DOC.querySelector(".movie-list:is(.h, .cols-3) .cover")) this.listMovieVideo(DOC);
  3678. return;
  3679. }
  3680.  
  3681. this.listScroll({
  3682. container,
  3683. itemSelector: [
  3684. ".movie-list:has(.tags) .item",
  3685. ".actors .box",
  3686. ".section-container .box",
  3687. ".common-list ul .list-item",
  3688. ],
  3689. path: ".pagination-next",
  3690. start: items => {
  3691. container.innerHTML = "";
  3692. container.append(...items);
  3693. container.classList.add("x-grid");
  3694. },
  3695. showPage: () => DOC.querySelector("nav.pagination")?.classList.add("x-flex-center"),
  3696. onLoad: items => container.append(...items),
  3697. });
  3698. },
  3699. modifyItem(items, isMerge) {
  3700. const isMovieList = items.some(item => item.querySelector(".video-title"));
  3701. const res = [];
  3702. const hlUrls = [];
  3703.  
  3704. for (let index = 0, { length } = items; index < length; index++) {
  3705. const item = items[index];
  3706. let code = "";
  3707. let date = "";
  3708. let score = 0;
  3709. let highlight = false;
  3710.  
  3711. if (isMovieList) {
  3712. code = item.querySelector(".video-title strong");
  3713. if (!code) continue;
  3714.  
  3715. code = code.textContent.trim();
  3716. date = item.querySelector(".meta")?.textContent ?? "";
  3717. date = date.replaceAll("-", "").trim();
  3718.  
  3719. if (isMerge) {
  3720. if (res.some(t => t.code === code && t.date === date)) continue;
  3721. score = (item.querySelector(".score")?.textContent ?? "").match(NUM_REGEX)[0] ?? 0;
  3722. item.querySelector(".meta-buttons")?.remove();
  3723. }
  3724.  
  3725. this.modifyMovieCard(item);
  3726. if (item.matches(".x-hide")) continue;
  3727.  
  3728. highlight = item.matches(".x-highlight");
  3729. if (highlight) hlUrls.push(item.querySelector("a").href);
  3730. }
  3731.  
  3732. fadeInImg(item);
  3733. res.push({ code, date, score, highlight, item });
  3734. }
  3735. if (!res.length) return res;
  3736.  
  3737. if (isMovieList) {
  3738. this.driveMatchForList(res, this.click.upQuery);
  3739. addPrefetch(hlUrls);
  3740. if (isMerge) {
  3741. res.sort((pre, next) => {
  3742. return pre.score === next.score ? next.date - pre.date : next.score - pre.score;
  3743. });
  3744. }
  3745. res.sort((pre, next) => (pre.highlight > next.highlight ? -1 : 1));
  3746. }
  3747. return res.map(({ item }) => item);
  3748. },
  3749. modifyMovieCard(item) {
  3750. const titleNode = item.querySelector(".video-title");
  3751. const code = titleNode.querySelector("strong").textContent.trim();
  3752. const title = titleNode.textContent.replace(code, "").trim();
  3753. const score = item
  3754. .querySelector(".score .value")
  3755. .textContent.replace(/&nbsp;/g, "")
  3756. .trim();
  3757. const tags = item.querySelectorAll(".tags .tag");
  3758.  
  3759. const filterClass = this.listFilter({ code, title, score, tags });
  3760. if (filterClass) item.classList.add(filterClass);
  3761. if (filterClass === "x-hide") return;
  3762.  
  3763. titleNode.insertAdjacentHTML(
  3764. "afterbegin",
  3765. `<span class="x-btn" title="列表离线" data-code="${code}" data-title="${title}"></span>`
  3766. );
  3767. if (item.querySelector(".tags")) titleNode.classList.add("x-ellipsis");
  3768. titleNode.classList.add("x-title");
  3769. },
  3770. };
  3771. movie = {
  3772. info: {},
  3773. params: {},
  3774. magnets: [],
  3775.  
  3776. docStart() {
  3777. const style = `
  3778. .video-meta-panel {
  3779. margin: -10px 0 20px;
  3780. padding: 0;
  3781. }
  3782. .video-meta-panel > .columns {
  3783. margin: 0;
  3784. align-items: start;
  3785. }
  3786. .video-meta-panel > .columns > .column {
  3787. padding: 0 10px;
  3788. }
  3789. .video-meta-panel > .columns > .column-video-cover {
  3790. margin: 10px;
  3791. padding: 0;
  3792. aspect-ratio: var(--x-cover-ratio);
  3793. }
  3794. .column-video-cover a:has(> :is(img, video)),
  3795. .column-video-cover .cover-container::after,
  3796. .preview-video-container::after {
  3797. width: 100%;
  3798. height: 100%;
  3799. }
  3800. img, video {
  3801. vertical-align: top;
  3802. }
  3803. :is(.column-video-cover, .tile-images) a:has(> :is(img, video)) {
  3804. display: block;
  3805. background: var(--x-sub-ftc);
  3806. }
  3807. .tile-images a:has(> img) {
  3808. aspect-ratio: var(--x-thumb-ratio);
  3809. }
  3810. .preview-images a:has(> img) {
  3811. aspect-ratio: var(--x-sprite-ratio);
  3812. }
  3813. :root[data-theme="dark"] :is(.column-video-cover, .tile-images) a:has(> :is(img, video)) {
  3814. background: var(--x-grey) !important;
  3815. }
  3816. :is(.column-video-cover, .tile-images) :is(img, video) {
  3817. width: 100% !important;
  3818. height: 100% !important;
  3819. max-height: unset !important;
  3820. object-fit: contain;
  3821. }
  3822. .movie-panel-info div.panel-block {
  3823. padding: 10px 0;
  3824. font-size: 14px;
  3825. line-height: var(--x-line-h);
  3826. }
  3827. :root[data-theme="dark"] .panel .panel-block:last-child {
  3828. border-bottom: none;
  3829. }
  3830. .panel-block:has(#x-res) {
  3831. padding-bottom: 0;
  3832. border-bottom: 0;
  3833. }
  3834. .panel-block:has(#x-match) {
  3835. border-bottom: 0;
  3836. }
  3837. .panel-block:has(#toolbar) {
  3838. padding-top: 0;
  3839. }
  3840. .movie-panel-info .copy-to-clipboard {
  3841. height: var(--x-line-h);
  3842. }
  3843. .video-detail > .columns {
  3844. margin-bottom: 8px;
  3845. }
  3846. .message-header,
  3847. .message-body,
  3848. #magnets-content > .columns > .column {
  3849. padding: 10px;
  3850. }
  3851. .video-panel .tile-images, .plain-grid-list {
  3852. gap: 10px;
  3853. }
  3854. .video-panel .tile-images .tile-item .video-number {
  3855. padding-top: 6px;
  3856. }
  3857. .columns[data-controller="movie-tab"],
  3858. #magnets-content > .columns,
  3859. .plain-grid-list a.box {
  3860. margin: 0;
  3861. }
  3862. .columns[data-controller="movie-tab"] > .column,
  3863. .video-panel .message-body .top-meta {
  3864. padding: 0;
  3865. }
  3866. #tabs-container .message {
  3867. margin-bottom: 20px !important;
  3868. }
  3869. .top-meta :is(.button.is-info, .moj-content) {
  3870. display: none;
  3871. }
  3872. #reviews .review-items .review-item {
  3873. padding: 10px 0;
  3874. }
  3875. #reviews .review-items .review-item:first-child {
  3876. padding-top: 0;
  3877. }
  3878. #reviews .review-items .review-item:last-child {
  3879. padding-bottom: 0;
  3880. }
  3881. .plain-grid-list .item,
  3882. nav.pagination.no-line:has(a) {
  3883. display: flex;
  3884. }
  3885. .plain-grid-list .item {
  3886. align-items: baseline;
  3887. }
  3888. .plain-grid-list .item strong {
  3889. flex: 1;
  3890. padding-right: 10px;
  3891. overflow: hidden;
  3892. white-space: nowrap;
  3893. text-overflow: ellipsis;
  3894. }
  3895. .column-video-cover .cover-container {
  3896. position: absolute;
  3897. top: 50%;
  3898. left: 50%;
  3899. transform: translate(-50%, -50%);
  3900. }
  3901. .isPlayer .cover-container {
  3902. top: 25%;
  3903. }
  3904. .column-video-cover .cover-container .play-button {
  3905. position: unset;
  3906. transform: none;
  3907. }
  3908. .column-video-cover a[id] {
  3909. opacity: 0;
  3910. position: absolute;
  3911. top: 0;
  3912. left: 0;
  3913. }
  3914. .column-video-cover a[id].active {
  3915. z-index: 1;
  3916. opacity: 1;
  3917. transition: opacity .25s linear;
  3918. }
  3919. .column-video-cover video {
  3920. display: inline !important;
  3921. }
  3922. #x-res, #toolbar {
  3923. width: 100%;
  3924. }
  3925. :is(#x-res, #toolbar) button {
  3926. flex: 1;
  3927. }
  3928. .tags:has(.x-jump) {
  3929. margin-bottom: -5px;
  3930. }
  3931. html:has(.fancybox-is-open) {
  3932. overflow: hidden;
  3933. }
  3934. .fancybox-slide--image.fancybox-slide--current {
  3935. overflow: overlay;
  3936. }
  3937. .fancybox-slide--image.fancybox-slide--current .fancybox-content {
  3938. left: 50%;
  3939. width: fit-content !important;
  3940. height: auto !important;
  3941. padding: 44px 0;
  3942. transform: translateX(-50%) !important;
  3943. }
  3944. .fancybox-slide--image.fancybox-slide--current .fancybox-content img {
  3945. position: static;
  3946. width: auto;
  3947. max-width: calc(100vw - 140px);
  3948. height: auto;
  3949. }
  3950. .top-meta:has(.tag) {
  3951. margin-bottom: 10px;
  3952. padding-right: 10px !important;
  3953. }
  3954. .top-meta .tags {
  3955. flex: 1;
  3956. margin: 0 0 -8px;
  3957. }
  3958. #magnets-content {
  3959. ${this.M_LINE > 0 ? `max-height: calc(45px * ${this.M_LINE});` : ""}
  3960. overscroll-behavior-y: contain;
  3961. overflow: overlay;
  3962. }
  3963. :root[data-theme="dark"] #magnets-content::-webkit-scrollbar-thumb {
  3964. background: #4a4a4a !important;
  3965. }
  3966. #magnets-content .item {
  3967. height: 45px;
  3968. border-bottom: 1px solid #ededed;
  3969. }
  3970. :root[data-theme=dark] #magnets-content .item {
  3971. border-color: #4a4a4a;
  3972. }
  3973. #magnets-content .item:last-child {
  3974. border: none;
  3975. }
  3976. #magnets-content .item .column:first-child {
  3977. flex: 3;
  3978. font-weight: bold;
  3979. }
  3980. #magnets-content .item .column:is(:nth-child(1), :nth-child(2), :nth-child(3)) {
  3981. text-align: left;
  3982. }
  3983. #magnets-content .item .column.x-line {
  3984. max-width: 180px;
  3985. }
  3986. #magnets-content .x-from {
  3987. min-width: 65px;
  3988. }
  3989. #magnets-content .item .column:last-child {
  3990. flex: none;
  3991. text-align: right;
  3992. }
  3993. #x-total {
  3994. padding: 10px 10px 0;
  3995. text-align: right;
  3996. }
  3997. #toolbar button.active,
  3998. #magnets-content a[data-magnet].active {
  3999. opacity: .5;
  4000. }
  4001. `;
  4002. this.globalDark(`${this._style.common}${style}`);
  4003. this.listMerge();
  4004. },
  4005. async contentLoaded() {
  4006. this.globalSearch();
  4007. this.modifyLayout();
  4008.  
  4009. const titleNode = DOC.querySelector("h2.title");
  4010. const currentTitle = titleNode.querySelector(".current-title");
  4011. const originTitle = titleNode.querySelector(".origin-title");
  4012. const code = DOC.querySelector(".first-block .value").textContent;
  4013. const title = (originTitle ?? currentTitle).textContent.replace(code, "").trim();
  4014. this.info = {
  4015. code: /^FC2-\d/i.test(code) ? code.replace("-", "-PPV-") : code,
  4016. title,
  4017. transTitle: originTitle ? currentTitle.textContent.trim() : "",
  4018. cover: DOC.querySelector(".column-video-cover img").src,
  4019. isVR: VR_REGEX.test(title),
  4020. studio: "",
  4021. series: "",
  4022. genre: [],
  4023. star: [],
  4024. video: "",
  4025. };
  4026. const video = DOC.querySelector("#preview-video source")?.getAttribute("src") ?? "";
  4027. if (video && (await request(video, {}, "HEAD"))) this.info.video = video;
  4028. for (const item of DOC.querySelectorAll(".movie-panel-info > .panel-block")) {
  4029. let [label, value] = item.querySelectorAll(["strong", ".value"]);
  4030. if (!label || !value) continue;
  4031.  
  4032. label = label?.textContent?.trim();
  4033. if (!label) continue;
  4034.  
  4035. if (["片商:", "賣家:"].includes(label)) this.info.studio = value.textContent.trim();
  4036. if (label === "系列:") this.info.series = value.textContent.trim();
  4037. if (label === "評分:") item.classList.add("score");
  4038. if (label === "類別:") {
  4039. item.classList.add("genre");
  4040. if (!value.querySelector("a")) continue;
  4041. this.info.genre = Array.from(value.querySelectorAll("a")).map(item => item.textContent);
  4042. }
  4043. if (label === "演員:") {
  4044. item.classList.add("star");
  4045. if (!value.querySelector("a")) continue;
  4046. const male = [];
  4047. const female = [];
  4048. value.textContent
  4049. .split("\n")
  4050. .map(item => item.trim())
  4051. .filter(Boolean)
  4052. .forEach(item => {
  4053. (item.includes("♂") ? male : female).push(item.replace(/♂|♀/, ""));
  4054. });
  4055. this.info.star = [...female, ...male];
  4056. }
  4057. }
  4058. this.getMovieResource(this.info);
  4059. if (this.M_COPY) {
  4060. titleNode.insertAdjacentHTML(
  4061. "beforeend",
  4062. `&nbsp;<a class="copy-to-clipboard" title="原始标题" data-clipboard-text="${code} ${title}">复制</a>`
  4063. );
  4064. GM_addStyle("#magnets-content a[data-copy]{display:inline-flex!important}");
  4065. DOC.querySelector(".panel-block.star .value").addEventListener("contextmenu", e => {
  4066. const { target } = e;
  4067. if (target.matches("a")) handleCopy(e, "", target.textContent);
  4068. });
  4069. }
  4070.  
  4071. this.movieRes();
  4072. this.movieTitle();
  4073. this._movieJump();
  4074. this.movieScore();
  4075. this.movieStar();
  4076. this.driveMatch();
  4077. this.refactorTable();
  4078. },
  4079. modifyLayout() {
  4080. if (history.scrollRestoration) history.scrollRestoration = "manual";
  4081. window.scrollTo({ top: 150, left: 0, behavior: "smooth" });
  4082. this.modifyItem();
  4083.  
  4084. unsafeWindow.$.fancybox.defaults.loop = true;
  4085. unsafeWindow.$.fancybox.defaults.smallBtn = false;
  4086. unsafeWindow.$.fancybox.defaults.toolbar = true;
  4087. unsafeWindow.$.fancybox.defaults.buttons = ["slideShow", "thumbs", "close"];
  4088. unsafeWindow.$.fancybox.defaults.animationEffect = "fade";
  4089. unsafeWindow.$.fancybox.defaults.animationDuration = 300;
  4090. unsafeWindow.$.fancybox.defaults.transitionDuration = 150;
  4091. unsafeWindow.$.fancybox.defaults.touch = { vertical: false };
  4092. unsafeWindow.$.fancybox.defaults.wheel = false;
  4093. unsafeWindow.$.fancybox.defaults.clickContent = false;
  4094.  
  4095. const cover = DOC.querySelector(".cover-container");
  4096. if (!cover) return;
  4097.  
  4098. const _cover = cover.cloneNode();
  4099. const playBtn = cover.querySelector(".play-button");
  4100. _cover.append(playBtn.cloneNode(true));
  4101. playBtn.replaceWith(_cover);
  4102. _cover.addEventListener("click", e => e.stopPropagation());
  4103.  
  4104. cover.removeAttribute("rel");
  4105. cover.removeAttribute("class");
  4106. cover.removeAttribute("target");
  4107. cover.href = cover.querySelector("img").src;
  4108. cover.setAttribute("data-fancybox", "gallery");
  4109. },
  4110. modifyItem() {
  4111. const selectors = [".tile-images.tile-small .tile-item"];
  4112. const codeQuery = ".video-number";
  4113. const upQuery = "a:has(> img)";
  4114.  
  4115. const res = [];
  4116. for (const item of DOC.querySelectorAll(selectors)) {
  4117. const img = item.querySelector("img");
  4118. if (!img) continue;
  4119.  
  4120. img.replaceWith(DOC.create("a", { href: VOID }, img.cloneNode()));
  4121. const code = item.querySelector(codeQuery)?.textContent;
  4122. if (!code) continue;
  4123.  
  4124. item.querySelector(".video-title")?.classList.add("x-title");
  4125. res.push({ item, code });
  4126. }
  4127. this.driveMatchForList(res, upQuery);
  4128.  
  4129. this.click.selectors = selectors;
  4130. this.click.codeQuery = codeQuery;
  4131. this.click.upQuery = upQuery;
  4132. this.globalClick();
  4133. },
  4134. movieRes() {
  4135. const { res } = this.params;
  4136. if (!res?.length) return;
  4137.  
  4138. const box = DOC.querySelector(".column-video-cover");
  4139. const info = DOC.querySelector(".movie-panel-info");
  4140. const first = "x-cover";
  4141. const firstQuery = `button[for=${first}]`;
  4142.  
  4143. const cover = box.querySelector("a[data-fancybox]");
  4144. cover.setAttribute("data-fancybox", "tabbar");
  4145. cover.setAttribute("id", first);
  4146. cover.classList.add("active");
  4147. box.insertAdjacentHTML(
  4148. "afterbegin",
  4149. '<div class="x-side prev"><span>🔙</span></div><div class="x-side next"><span>🔜</span></div>'
  4150. );
  4151. info.insertAdjacentHTML(
  4152. "afterbegin",
  4153. `<div class="panel-block">
  4154. <div id="x-res" class="buttons has-addons are-small">
  4155. <button class="button is-light is-active" for="${first}">封面</button>
  4156. ${res
  4157. .map(
  4158. item =>
  4159. `<button class="button is-light is-loading" for="x-${item}" disabled>暂无</button>`
  4160. )
  4161. .join("")}
  4162. </div>
  4163. </div>`
  4164. );
  4165.  
  4166. box.addEventListener("click", e => {
  4167. const { target } = e;
  4168.  
  4169. const side = target.closest(".x-side");
  4170. if (side) {
  4171. e.stopPropagation();
  4172. e.preventDefault();
  4173.  
  4174. const navs = info.querySelectorAll("#x-res button:not([disabled])");
  4175. const { length } = navs;
  4176. if (length === 1) return;
  4177.  
  4178. let index = Array.from(navs).findIndex(item => item.classList.contains("is-active"));
  4179. index = side.classList.contains("next") ? index + 1 : index - 1;
  4180. if (index > length - 1) index = 0;
  4181. if (index === -1) index = length - 1;
  4182. return navs[index].click();
  4183. }
  4184.  
  4185. if (target.closest(".x-player")) {
  4186. e.stopPropagation();
  4187. e.preventDefault();
  4188. return box.querySelector(".x-side.next").click();
  4189. }
  4190. });
  4191. info.querySelector("#x-res").addEventListener("click", ({ target }) => {
  4192. const tag = target.getAttribute("for");
  4193. if (!tag) return;
  4194.  
  4195. const active = box.querySelector(`#${tag}`);
  4196. if (target.classList.contains("is-active")) return active.querySelector("video")?.focus();
  4197.  
  4198. const _target = info.querySelector("#x-res .is-active");
  4199. _target.classList.toggle("is-active");
  4200. if (_target.matches(firstQuery)) box.classList.remove("x-player");
  4201.  
  4202. target.classList.toggle("is-active");
  4203. if (target.matches(firstQuery) && box.classList.contains("isPlayer")) {
  4204. box.classList.add("x-player");
  4205. }
  4206.  
  4207. const _active = box.querySelector(".active");
  4208. _active.classList.toggle("active");
  4209. _active.querySelector("video")?.pause();
  4210.  
  4211. active.classList.toggle("active");
  4212. const video = active.querySelector("video");
  4213. if (!video) return;
  4214.  
  4215. video.focus();
  4216. video.play();
  4217. });
  4218. res.forEach(key => {
  4219. this.upMovieResource(key, val => {
  4220. const id = `x-${key}`;
  4221. const nav = info.querySelector(`button[for=${id}]`);
  4222. nav.classList.remove("is-loading");
  4223. if (!val.length) return;
  4224.  
  4225. this.info[key] = val;
  4226. nav.removeAttribute("disabled");
  4227. nav.innerHTML = TAB_NAME[key];
  4228.  
  4229. const targetId = `${id}-target`;
  4230. const node = key === "img" ? DOC.create(key, { src: val }) : createVideo(val, { id: targetId });
  4231. const container = DOC.create(
  4232. "a",
  4233. { "data-fancybox": "tabbar", id, href: key === "img" ? val : `#${targetId}` },
  4234. node
  4235. );
  4236. box.append(container);
  4237. if (key === "img" || !nav.previousElementSibling?.matches(firstQuery)) return;
  4238. box.classList.add("isPlayer", "x-player");
  4239. });
  4240. });
  4241. },
  4242. movieTitle() {
  4243. this.upMovieResource(
  4244. "title",
  4245. val => {
  4246. DOC.querySelector("#x-title").textContent = val.length ? val : "暂无数据";
  4247. },
  4248. () => {
  4249. DOC.querySelector(".panel-block.first-block").insertAdjacentHTML(
  4250. "beforebegin",
  4251. '<div class="panel-block"><strong>机翻:</strong>&nbsp;<span id="x-title" class="value">查询中...</span></div>'
  4252. );
  4253. }
  4254. );
  4255. },
  4256. _movieJump() {
  4257. this.movieJump(
  4258. this.info,
  4259. res => {
  4260. DOC.querySelector(".panel-block.first-block").insertAdjacentHTML(
  4261. "afterend",
  4262. res
  4263. .map(
  4264. ({ group, list }) =>
  4265. `<div class="panel-block"><strong>${group}:</strong>&nbsp;<div class="tags">${list
  4266. .map(
  4267. ({ url, query, name }) =>
  4268. `<span class="x-jump tag is-light${
  4269. query ? " is-info" : ""
  4270. }" data-query="${query}" data-href="${url}">${name}</span>`
  4271. )
  4272. .join("")}</div></div>`
  4273. )
  4274. .join("")
  4275. );
  4276. },
  4277. (res, { classList }) => {
  4278. classList.remove("is-light");
  4279. if (res.zh) classList.replace("is-info", "is-warning");
  4280. }
  4281. );
  4282. },
  4283. movieScore() {
  4284. const { score } = this.params;
  4285. if (!score?.length) return;
  4286.  
  4287. const scoreTarget = DOC.querySelector(".panel-block.score");
  4288. const insertTarget = scoreTarget ?? DOC.querySelector(".panel-block.genre, .panel-block.star");
  4289. const insertScore = (position, item) => {
  4290. insertTarget?.insertAdjacentHTML(
  4291. position,
  4292. `<div class="panel-block"><strong>${item.toUpperCase()}评分:</strong>&nbsp;<span id="x-${item}_score" class="value">查询中...</span></div>`
  4293. );
  4294. };
  4295.  
  4296. let position = "beforebegin";
  4297. for (const item of score) {
  4298. if (item === "db") {
  4299. if (scoreTarget) {
  4300. scoreTarget.querySelector("strong").innerHTML = "DB评分:";
  4301. position = "afterend";
  4302. }
  4303. continue;
  4304. }
  4305. insertScore(position, item);
  4306. }
  4307. score
  4308. .filter(key => key !== "db")
  4309. .forEach(key => {
  4310. key = `${key}_score`;
  4311. this.upMovieResource(key, val => {
  4312. const node = DOC.querySelector(`#x-${key}`);
  4313.  
  4314. if (!val.length) {
  4315. node.textContent = "暂无数据";
  4316. return;
  4317. }
  4318.  
  4319. const { score, total, num } = val[0];
  4320. let stars = Math.floor((score / total) * 5);
  4321. stars = Array(5).fill("", 0, stars).fill(" gray", stars, 5);
  4322.  
  4323. node.innerHTML = `<span class="score-stars">${stars
  4324. .map(item => `<i class="icon-star${item}"></i>`)
  4325. .join("")}</span>&nbsp;${score}分${num ? `, ${num}人评价` : ""}`;
  4326. });
  4327. });
  4328. },
  4329. movieStar() {
  4330. const target = DOC.querySelector(".panel-block.star .value");
  4331. this.upMovieResource(
  4332. "star",
  4333. val => {
  4334. this.info.star = val;
  4335. target.innerHTML = !val.length
  4336. ? "暂无数据"
  4337. : val
  4338. .map(
  4339. item =>
  4340. `<a href="/search?f=actor&q=${item}">${item}</a><strong class="symbol female">♀</strong>`
  4341. )
  4342. .join("&nbsp;");
  4343. },
  4344. () => {
  4345. target.innerHTML = "查询中...";
  4346. }
  4347. );
  4348. },
  4349. refactorTable() {
  4350. let caption = DOC.querySelector(".top-meta");
  4351. caption.classList.add("is-flex");
  4352. caption.insertAdjacentHTML("afterbegin", '<div class="tags"></div>');
  4353. caption = caption.querySelector(".tags");
  4354. const table = DOC.querySelector("#magnets-content");
  4355. table.parentElement.insertAdjacentHTML("beforeend", '<div id="x-total">总数 0</div>');
  4356.  
  4357. const magnets = [];
  4358. for (const item of table.querySelectorAll(".item")) {
  4359. const first = item.querySelector("a");
  4360. if (!first) continue;
  4361.  
  4362. const size = first.querySelector(".meta")?.textContent?.trim() ?? "";
  4363. magnets.push({
  4364. name: first.querySelector(".name").textContent,
  4365. link: first.href.split("&")[0],
  4366. size,
  4367. bytes: transToBytes(size),
  4368. zh: !!first.querySelector(".tag.is-warning.is-small.is-light"),
  4369. date: item.querySelector(".time")?.textContent ?? "",
  4370. });
  4371. }
  4372. table.innerHTML = "暂无数据";
  4373. this.refactorTbody(magnets);
  4374.  
  4375. (this.params?.magnet ?? []).forEach(key => {
  4376. key = `${key}_magnet`;
  4377. this.upMovieResource(
  4378. key,
  4379. res => this.refactorTbody(res, key),
  4380. () => {
  4381. caption.insertAdjacentHTML(
  4382. "beforeend",
  4383. `<span class="tag is-success is-light" id="x-${key}">${key
  4384. .split("_")[0]
  4385. .toUpperCase()}搜索</span>`
  4386. );
  4387. }
  4388. );
  4389. });
  4390. },
  4391. refactorTbody(magnets, key) {
  4392. if (!magnets.length) return;
  4393.  
  4394. if (key) DOC.querySelector(`#x-${key}`)?.classList.remove("is-light");
  4395. let start;
  4396.  
  4397. if (this.magnets.length) {
  4398. for (const item of magnets) {
  4399. const { link, zh } = item;
  4400. const index = this.magnets.findIndex(item => item.link.toLowerCase() === link.toLowerCase());
  4401. if (index === -1) {
  4402. this.magnets.push(item);
  4403. continue;
  4404. }
  4405. if (zh) this.magnets[index].zh = zh;
  4406. }
  4407. magnets = this.magnets;
  4408. } else {
  4409. const caption = DOC.querySelector(".top-meta");
  4410. start = () => {
  4411. caption
  4412. .querySelector(".tags")
  4413. .insertAdjacentHTML("beforeend", '<span class="tag is-success">磁力排序</span>');
  4414. };
  4415.  
  4416. if (this.M_COPY) {
  4417. caption.insertAdjacentHTML(
  4418. "beforeend",
  4419. `<a href="${VOID}" data-copy="all" title="复制全部磁链" class="tag is-info">复制全部</a>`
  4420. );
  4421. }
  4422.  
  4423. DOC.querySelector("#magnets .message-body").addEventListener("click", e => {
  4424. const { copy, magnet } = e.target.dataset;
  4425. if (!copy && !magnet) return;
  4426.  
  4427. if (magnet) return this._driveOffLine(e);
  4428. if (copy !== "all") return handleCopy(e);
  4429. handleCopy(e, "", this.magnets.map(item => item.link).join("\n"));
  4430. });
  4431. }
  4432. const total = DOC.querySelector("#x-total");
  4433. if (total) total.textContent = `总数 ${magnets.length}`;
  4434. magnets = this.movieSort(magnets, start);
  4435. this.magnets = magnets;
  4436. DOC.querySelector("#magnets-content").innerHTML = this.refactorTr(magnets);
  4437. },
  4438. refactorTr(magnets) {
  4439. return magnets
  4440. .map(
  4441. ({ name, link, size, date, from, href, zh }) =>
  4442. `<div class="item columns odd">
  4443. <div class="column" title="${name}">
  4444. <a href="${link}" class="x-ellipsis">${name}</a>
  4445. </div>
  4446. <div class="column x-line" title="${size}">${size}</div>
  4447. <div class="column x-line" title="${date}">${date}</div>
  4448. <div class="column">
  4449. <a
  4450. class="tag is-danger is-light x-from"
  4451. href="${href || VOID}"
  4452. ${href ? ' target="_blank" title="查看详情"' : ""}
  4453. >${from || Domain}</a>
  4454. </div>
  4455. <div class="column">
  4456. ${zh ? '<span class="tag is-warning is-light">字幕</span>' : ""}
  4457. </div>
  4458. <div class="column">
  4459. <a
  4460. href="${VOID}"
  4461. class="tag is-info is-hidden"
  4462. title="复制磁力链接"
  4463. data-copy="${link}"
  4464. >复制链接</a><a
  4465. href="${VOID}"
  4466. class="tag is-info x-ml is-hidden"
  4467. title="添加离线任务"
  4468. data-magnet="${link}"
  4469. >添加离线</a>
  4470. </div>
  4471. </div>`
  4472. )
  4473. .join("");
  4474. },
  4475. driveMatch() {
  4476. this.driveMatchForMovie(
  4477. this.info,
  4478. res => {
  4479. const refresh = DOC.querySelector("#x-refresh");
  4480. refresh.inert = false;
  4481. refresh.textContent = "刷新资源";
  4482. refresh.classList.remove("active");
  4483.  
  4484. DOC.querySelector("#x-match").innerHTML = !res.length
  4485. ? "暂无数据"
  4486. : res
  4487. .map(
  4488. ({ n, pc, fid, cid, t }) =>
  4489. `<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>`
  4490. )
  4491. .join("");
  4492. },
  4493. () => {
  4494. const refresh = DOC.querySelector("#x-refresh");
  4495. if (refresh) {
  4496. refresh.inert = true;
  4497. refresh.textContent = "请求中...";
  4498. return refresh.classList.add("active");
  4499. }
  4500.  
  4501. const { icon, resources } = GM_info.script;
  4502. addPrefetch([icon, ...resources.map(item => item.url)]);
  4503. GM_addStyle("#magnets-content a[data-magnet]{display:inline-flex!important}");
  4504. DOC.querySelector(".movie-panel-info").insertAdjacentHTML(
  4505. "beforeend",
  4506. '<div class="panel-block"><strong>资源:</strong>&nbsp;<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>'
  4507. );
  4508. DOC.querySelector("#x-refresh").addEventListener("click", () => this.driveMatch());
  4509. DOC.querySelector("#x-magnet").addEventListener("click", e => this._driveOffLine(e));
  4510. const modify = this.upModifyTarget();
  4511. DOC.querySelector("#x-match").addEventListener("click", e => {
  4512. if (e.target.dataset.pc) this.advancedLink(e, { type: "left" });
  4513. if (e.target.classList.contains("x-btn")) this.driveModify(e, modify);
  4514. });
  4515. DOC.querySelector("#x-match").addEventListener("contextmenu", e => {
  4516. if (e.target.dataset.cid) this.advancedLink(e, { type: "right" });
  4517. });
  4518. }
  4519. );
  4520. },
  4521. _driveOffLine(e) {
  4522. this.driveOffLine(e, { ...this.info, magnets: this.magnets });
  4523. },
  4524. };
  4525. others = {
  4526. docStart() {
  4527. this.globalDark(this._style.common);
  4528. this.listMerge();
  4529. },
  4530. contentLoaded() {
  4531. this.globalSearch();
  4532. },
  4533. };
  4534. }
  4535. class JavBus extends Common {
  4536. constructor() {
  4537. super();
  4538. return super.init();
  4539. }
  4540.  
  4541. routes = {
  4542. list: /^\/((uncensored\/?)?(page\/\d+)?$)|((uncensored\/)?(((search|star)+|genre|studio|label|series|director|member)+\/)|actresses(\/\d+)?)+/i,
  4543. genre: /^\/(uncensored\/)?genre$/i,
  4544. forum: /^\/forum\//i,
  4545. movie: /^\/[a-z0-9]+(-|\w)+/i,
  4546. };
  4547. _style = {
  4548. common: `
  4549. body {
  4550. overflow-y: overlay;
  4551. }
  4552. .ad-box,
  4553. footer {
  4554. display: none;
  4555. }
  4556. `,
  4557. card: `
  4558. a:is(.movie-box, .avatar-box) {
  4559. width: var(--x-thumb-w);
  4560. margin: 10px !important;
  4561. }
  4562. .photo-frame {
  4563. height: auto !important;
  4564. margin: 10px !important;
  4565. background: var(--x-sub-ftc);
  4566. border: none;
  4567. }
  4568. .movie-box .photo-frame {
  4569. aspect-ratio: var(--x-thumb-ratio);
  4570. }
  4571. .avatar-box .photo-frame {
  4572. aspect-ratio: var(--x-avatar-ratio);
  4573. }
  4574. .photo-frame img {
  4575. width: 100%;
  4576. max-width: none !important;
  4577. height: 100% !important;
  4578. max-height: none !important;
  4579. margin: 0 !important;
  4580. object-fit: cover;
  4581. }
  4582. .photo-info {
  4583. height: auto !important;
  4584. padding: 0 10px 10px;
  4585. line-height: var(--x-line-h);
  4586. background: unset;
  4587. border: none;
  4588. }
  4589. `,
  4590. dark: `
  4591. :is(.nav, .dropdown-menu) > li > a:is(:hover, :focus) {
  4592. background: var(--x-grey) !important;
  4593. }
  4594. .nav > :is(li.active, .open) > a,
  4595. .nav > .open > a:is(:hover, :focus),
  4596. .dropdown-menu {
  4597. background: var(--x-bgc) !important;
  4598. }
  4599. .modal-content, .alert {
  4600. background: var(--x-sub-bgc) !important;
  4601. }
  4602. .btn-primary {
  4603. background: var(--x-blue) !important;
  4604. border: none;
  4605. }
  4606. .btn-success {
  4607. background: var(--x-green) !important;
  4608. border: none;
  4609. }
  4610. .btn-warning {
  4611. background: var(--x-orange) !important;
  4612. border: none;
  4613. }
  4614. .btn-danger {
  4615. background: var(--x-red) !important;
  4616. border: none;
  4617. }
  4618. .btn-link {
  4619. background: none !important;
  4620. border: none;
  4621. }
  4622. .btn.disabled, .btn[disabled], fieldset[disabled] .btn {
  4623. opacity: .8 !important;
  4624. }
  4625. `,
  4626. dmCard: `
  4627. .movie-box, .avatar-box {
  4628. background: var(--x-sub-bgc) !important;
  4629. }
  4630. .photo-frame {
  4631. background: var(--x-grey);
  4632. }
  4633. .photo-info {
  4634. color: unset;
  4635. }
  4636. .photo-info span date {
  4637. color: var(--x-sub-ftc);
  4638. }
  4639. `,
  4640. };
  4641. search = {
  4642. selectors: "#search-input",
  4643. pathname: `/search/${REP}`,
  4644. };
  4645. click = {
  4646. selectors: [".movie-box", ".avatar-box"],
  4647. codeQuery: "date",
  4648. upQuery: ".photo-frame",
  4649. };
  4650. merge = {
  4651. start: (list, active) => {
  4652. if (active) {
  4653. active = " active";
  4654. DOC.querySelector("#navbar .active").classList.remove("active");
  4655. }
  4656. DOC.querySelector("#navbar > .nav.navbar-nav")?.insertAdjacentHTML(
  4657. "beforeend",
  4658. `<li id="merge" class="dropdown hidden-sm${active}">
  4659. <a
  4660. href="#"
  4661. class="dropdown-toggle"
  4662. data-toggle="dropdown"
  4663. data-hover="dropdown"
  4664. role="button"
  4665. aria-expanded="false"
  4666. >合并列表 <span class="caret"></span></a>
  4667. <ul class="dropdown-menu" role="menu">
  4668. ${list.reduce((prev, curr) => `${prev}<li><a href="/?merge=${curr}">${curr}</a></li>`, "")}
  4669. </ul>
  4670. </li>`
  4671. );
  4672. },
  4673. };
  4674.  
  4675. list = {
  4676. imgReplace: [
  4677. {
  4678. regex: /\/thumbs?\//i,
  4679. replace: val => val.replace(/\/thumbs?\//g, "/cover/").replace(".jpg", "_b.jpg"),
  4680. },
  4681. {
  4682. regex: /pics\.dmm\.co\.jp/i,
  4683. replace: val => val.replace("ps.jpg", "pl.jpg"),
  4684. },
  4685. ],
  4686.  
  4687. docStart() {
  4688. const { common, card, dark, dmCard } = this._style;
  4689. const style = `
  4690. .alert-common, .alert-page {
  4691. margin-top: 20px;
  4692. }
  4693. .search-header {
  4694. padding: 0;
  4695. background: none;
  4696. box-shadow: none;
  4697. }
  4698. .search-header .nav-tabs, #waterfall {
  4699. display: none;
  4700. }
  4701. #waterfall, #waterfall img {
  4702. opacity: 0;
  4703. }
  4704. .text-center.hidden-xs {
  4705. display: none;
  4706. line-height: 0;
  4707. }
  4708. .pagination {
  4709. margin-bottom: 40px;
  4710. }
  4711. .movie-box .x-title + div {
  4712. height: var(--x-line-h) !important;
  4713. margin: 4px 0;
  4714. }
  4715. .mleft {
  4716. display: flex !important;
  4717. align-items: center;
  4718. }
  4719. .mleft .btn-xs {
  4720. margin: 0 6px 0 0 !important;
  4721. }
  4722. `;
  4723. const dmStyle = `
  4724. .nav-pills > li.active > a {
  4725. background-color: var(--x-blue) !important;
  4726. }
  4727. .pagination > li > a {
  4728. color: var(--x-ftc) !important;
  4729. background-color: var(--x-sub-bgc) !important;
  4730. }
  4731. .pagination > li:not(.active) > a:hover {
  4732. background-color: var(--x-grey) !important;
  4733. }
  4734. `;
  4735. this.globalDark(`${common}${style}${card}${this.listMovieTitle()}`, `${dark}${dmStyle}${dmCard}`);
  4736. this.listMerge();
  4737. this.upLOLTarget();
  4738. },
  4739. contentLoaded() {
  4740. this.globalSearch();
  4741. this.globalClick();
  4742. this.modifyLayout();
  4743. },
  4744. modifyLayout() {
  4745. DOC.querySelector(".search-header .nav")?.classList.replace("nav-tabs", "nav-pills");
  4746.  
  4747. const container = unsafeWindow.$("#waterfall");
  4748. this.listScroll({
  4749. container: "#waterfall",
  4750. path: "#next",
  4751. start: items => container.empty().append(items).show().masonry("reload"),
  4752. showPage: () => DOC.querySelector(".text-center.hidden-xs")?.classList.add("x-show"),
  4753. onLoad: items => {
  4754. items = unsafeWindow.$(items);
  4755. container.append(items).masonry("appended", items);
  4756. },
  4757. });
  4758. },
  4759. modifyItem(items, isMerge) {
  4760. const isMovieList = items.some(item => item.querySelector(".movie-box"));
  4761. const res = [];
  4762. const hlUrls = [];
  4763.  
  4764. for (let index = 0, { length } = items; index < length; index++) {
  4765. const item = items[index];
  4766. let code = "";
  4767. let date = "";
  4768. let highlight = false;
  4769.  
  4770. if (isMovieList) {
  4771. [code, date] = item.querySelectorAll("date");
  4772. if (!code || !date) continue;
  4773.  
  4774. code = code?.textContent.trim();
  4775. date = date?.textContent.replaceAll("-", "").trim();
  4776. if (!code || !date) continue;
  4777.  
  4778. if (isMerge && res.some(t => t.code === code && t.date === date)) continue;
  4779.  
  4780. this.modifyMovieCard(item);
  4781. if (item.querySelector(".x-hide")) continue;
  4782.  
  4783. highlight = item.querySelector(".x-highlight");
  4784. if (highlight) hlUrls.push(highlight.href);
  4785. } else {
  4786. this.modifyAvatarCard(item);
  4787. }
  4788.  
  4789. fadeInImg(item);
  4790. res.push({ code, date, highlight: !!highlight, item });
  4791. }
  4792. if (!res.length) return res;
  4793.  
  4794. if (isMovieList) {
  4795. this.driveMatchForList(res, this.click.upQuery);
  4796. addPrefetch(hlUrls);
  4797. if (isMerge) res.sort((pre, next) => next.date - pre.date);
  4798. res.sort((pre, next) => (pre.highlight > next.highlight ? -1 : 1));
  4799. }
  4800. return res.map(({ item }) => item);
  4801. },
  4802. modifyMovieCard(item) {
  4803. item = item.querySelector(".movie-box");
  4804. if (!item) return;
  4805.  
  4806. const info = item.querySelector(".photo-info span");
  4807. info.querySelector(".__cf_email__")?.remove();
  4808. info.innerHTML = info.innerHTML.replace("<br>", "");
  4809.  
  4810. const titleNode = info.firstChild;
  4811. const title = titleNode.textContent.trim();
  4812. const code = info.querySelector("date").textContent.trim();
  4813. const tags = info.querySelectorAll(".item-tag button");
  4814.  
  4815. const filterClass = this.listFilter({ code, title, tags });
  4816. if (filterClass) item.classList.add(filterClass);
  4817. if (filterClass === "x-hide") return;
  4818.  
  4819. this.listMovieImgType(item, this.imgReplace);
  4820. const reTitle = DOC.create("div", { title, class: "x-ellipsis x-title" }, title);
  4821. reTitle.insertAdjacentHTML(
  4822. "afterbegin",
  4823. `<span class="x-btn" title="列表离线" data-code="${code}" data-title="${title}"></span>`
  4824. );
  4825. titleNode.replaceWith(reTitle);
  4826. },
  4827. modifyAvatarCard(item) {
  4828. item = item.querySelector(".avatar-box");
  4829. if (!item) return;
  4830.  
  4831. const info = item.querySelector("span");
  4832. if (!info.classList.contains("mleft")) return info.classList.add("x-line");
  4833.  
  4834. const titleNode = info.firstChild;
  4835. const title = titleNode.textContent.trim();
  4836. titleNode.replaceWith(DOC.create("div", { title, class: "x-line" }, title));
  4837. info.insertAdjacentElement("afterbegin", info.querySelector("button"));
  4838. },
  4839. };
  4840. genre = {
  4841. docStart() {
  4842. const { common, dark } = this._style;
  4843. const style =
  4844. ":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}";
  4845. const dmStyle = ".genre-box{background:var(--x-sub-bgc)!important}";
  4846. this.globalDark(`${common}${style}`, `${dark}${dmStyle}`);
  4847. this.listMerge();
  4848. },
  4849. contentLoaded() {
  4850. this.globalSearch();
  4851. if (!DOC.querySelector("button.btn.btn-danger.btn-block.btn-genre")) return;
  4852.  
  4853. const box = DOC.querySelectorAll(".genre-box");
  4854. box[box.length - 1].classList.add("x-last-box");
  4855.  
  4856. DOC.querySelector(".container-fluid.pt10").addEventListener("click", ({ target }) => {
  4857. if (target.nodeName !== "A" || !target.classList.contains("text-center")) return;
  4858. const checkbox = target.querySelector("input");
  4859. checkbox.checked = !checkbox.checked;
  4860. });
  4861. },
  4862. };
  4863. forum = {
  4864. docStart() {
  4865. const style =
  4866. "#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)}";
  4867. const ifDark = this.G_DARK
  4868. ? "::-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}"
  4869. : "";
  4870. this.globalDark(`${this._style.common}${style}${ifDark}`);
  4871.  
  4872. this.merge.start = list => {
  4873. DOC.querySelector("#toptb ul")?.insertAdjacentHTML(
  4874. "beforeend",
  4875. `<li class="nav-title nav-inactive"><a href="/?merge=${list[0]}">合并列表</a></li>`
  4876. );
  4877. };
  4878. this.listMerge();
  4879. },
  4880. contentLoaded() {
  4881. this.globalSearch();
  4882. },
  4883. };
  4884. movie = {
  4885. info: {},
  4886. params: {},
  4887. magnets: [],
  4888.  
  4889. docStart() {
  4890. const { common, card, dark, dmCard } = this._style;
  4891. const style = `
  4892. #mag-submit-show,
  4893. #mag-submit,
  4894. #magnet-table,
  4895. h4[style="position:relative"],
  4896. h4[style="position:relative"] + .row,
  4897. .info .glyphicon-info-sign {
  4898. display: none !important;
  4899. }
  4900. html {
  4901. padding-right: 0 !important;
  4902. }
  4903. .container {
  4904. margin-bottom: 40px;
  4905. }
  4906. @media (width >= 1270px) {
  4907. .container {
  4908. width: 1270px;
  4909. }
  4910. }
  4911. .row.movie {
  4912. padding: 10px 0;
  4913. }
  4914. @media (width <= 480px) {
  4915. .row.movie {
  4916. padding: 0 !important;
  4917. }
  4918. }
  4919. .screencap, .info {
  4920. border: none !important;
  4921. padding: 0 10px;
  4922. }
  4923. .bigImage {
  4924. position: relative;
  4925. overflow: hidden;
  4926. display: block;
  4927. background: var(--x-sub-ftc);
  4928. aspect-ratio: var(--x-cover-ratio);
  4929. }
  4930. .bigImage :is(img, video) {
  4931. width: 100%;
  4932. height: 100%;
  4933. object-fit: contain;
  4934. }
  4935. .bigImage [id] {
  4936. opacity: 0;
  4937. position: absolute;
  4938. top: 0;
  4939. left: 0;
  4940. }
  4941. .bigImage .active {
  4942. z-index: 1;
  4943. opacity: 1 !important;
  4944. transition: opacity .25s linear;
  4945. }
  4946. @media (width <= 992px) {
  4947. .info {
  4948. padding-top: 10px !important;
  4949. }
  4950. }
  4951. .info p {
  4952. line-height: var(--x-line-h) !important;
  4953. }
  4954. .info :is(.header, .star-show, ul) {
  4955. margin-bottom: 0;
  4956. }
  4957. .info :is(.glyphicon-plus, .glyphicon-minus) {
  4958. margin-left: 4px;
  4959. font-size: 13px;
  4960. }
  4961. .star-box li {
  4962. background: var(--x-sub-ftc) !important;
  4963. }
  4964. .star-box img {
  4965. width: 100% !important;
  4966. height: 100% !important;
  4967. aspect-ratio: var(--x-avatar-ratio) !important;
  4968. font-size: 0;
  4969. margin: 0 !important;
  4970. }
  4971. .star-box .star-name {
  4972. padding: 6px 4px !important;
  4973. background: #fff;
  4974. border: none !important;
  4975. }
  4976. .star-box .star-name,
  4977. :is(.avatar-box, .movie-box) span {
  4978. display: block;
  4979. overflow: hidden;
  4980. white-space: nowrap;
  4981. text-overflow: ellipsis;
  4982. }
  4983. #magneturlpost + .movie {
  4984. margin-top: 20px !important;
  4985. padding: 10px !important;
  4986. }
  4987. #avatar-waterfall,
  4988. #sample-waterfall,
  4989. #related-waterfall {
  4990. margin: -10px !important;
  4991. word-spacing: -20px;
  4992. }
  4993. .avatar-box,
  4994. .sample-box,
  4995. .movie-box {
  4996. vertical-align: top !important;
  4997. word-spacing: 0 !important;
  4998. }
  4999. .avatar-box span {
  5000. padding-top: 0 !important;
  5001. background-color: unset !important;
  5002. border: none !important;
  5003. }
  5004. .sample-box {
  5005. margin: 10px !important;
  5006. width: var(--x-thumb-w) !important;
  5007. }
  5008. .sample-box .photo-frame {
  5009. aspect-ratio: var(--x-sprite-ratio);
  5010. }
  5011. .is-loading {
  5012. animation: spinAround .7s linear infinite;
  5013. }
  5014. @keyframes spinAround {
  5015. 0% { transform: rotate(0deg) }
  5016. to { transform: rotate(359deg) }
  5017. }
  5018. .x-star {
  5019. color: var(--x-orange);
  5020. }
  5021. .x-table {
  5022. margin: 0 !important;
  5023. }
  5024. .x-caption .label {
  5025. position: unset !important;
  5026. }
  5027. .x-table tr {
  5028. display: table;
  5029. width: 100%;
  5030. table-layout: fixed;
  5031. }
  5032. .x-table tr > * {
  5033. vertical-align: middle !important;
  5034. border-left: none !important;
  5035. }
  5036. .x-table tr > *:first-child {
  5037. width: 50px;
  5038. }
  5039. .x-table tr > *:nth-child(2) {
  5040. width: 33.3%;
  5041. }
  5042. .x-table tr > *:last-child,
  5043. .x-table tfoot tr > th:not(:nth-child(3)) {
  5044. border-right: none !important;
  5045. }
  5046. .x-table tbody {
  5047. display: block;
  5048. ${this.M_LINE > 0 ? `max-height: calc(39px * ${this.M_LINE} - 1px);` : ""}
  5049. overscroll-behavior-y: contain;
  5050. overflow: overlay;
  5051. table-layout: fixed;
  5052. }
  5053. .x-table tbody tr > * {
  5054. border-top: none !important;
  5055. }
  5056. .x-table tbody tr:last-child > *,
  5057. .x-table tfoot tr > * {
  5058. border-bottom: none !important;
  5059. }
  5060. .x-table code {
  5061. display: inline-block;
  5062. min-width: 65px;
  5063. }
  5064. .x-table td a:not(:last-child) {
  5065. margin-right: 10px;
  5066. }
  5067. #x-match a {
  5068. color: #CC0000 !important;
  5069. }
  5070. `;
  5071. const dmStyle = `
  5072. .bigImage,
  5073. .btn-group a,
  5074. .star-box li,
  5075. tbody tr:hover,
  5076. .table-striped > tbody > tr:nth-of-type(odd):hover,
  5077. .x-table code {
  5078. background: var(--x-grey) !important;
  5079. }
  5080. .btn-group a.active {
  5081. background: var(--x-bgc) !important;
  5082. }
  5083. .movie,
  5084. .btn-group a[disabled],
  5085. .star-box .star-name,
  5086. .sample-box,
  5087. .table-striped > tbody > tr:nth-of-type(odd) {
  5088. background: var(--x-sub-bgc) !important;
  5089. }
  5090. .avatar-box span {
  5091. color: unset !important;
  5092. }
  5093. `;
  5094. this.globalDark(`${common}${style}${card}`, `${dark}${dmStyle}${dmCard}`);
  5095. this.listMerge();
  5096. },
  5097. contentLoaded() {
  5098. this.globalSearch();
  5099. this.modifyItem();
  5100.  
  5101. const infoNode = DOC.querySelector(".info");
  5102. const info = infoNode.textContent;
  5103. const codeNode = infoNode.querySelector("span[style='color:#CC0000;']");
  5104. const code = codeNode.textContent;
  5105. const titleNode = DOC.querySelector("h3");
  5106. const title = titleNode.textContent.replace(code, "").trim();
  5107.  
  5108. this.info = {
  5109. code,
  5110. title,
  5111. cover: DOC.querySelector(".bigImage img").src,
  5112. isVR: VR_REGEX.test(title),
  5113. studio: info.match(/(?<=製作商: ).+/g)?.pop(0),
  5114. star: Array.from(infoNode.querySelectorAll("ul + p a")).map(item => item.textContent),
  5115. series: info.match(/(?<=系列: ).+/g)?.pop(0),
  5116. genre: Array.from(infoNode.querySelectorAll("p.header + p a")).map(item => item.textContent),
  5117. };
  5118. const cid = DOC.querySelector("#sample-waterfall a.sample-box")?.href;
  5119. if (cid?.includes("pics.dmm.co.jp")) this.info.cid = cid.split("/").at(-2);
  5120. this.getMovieResource(this.info);
  5121.  
  5122. if (this.M_COPY) {
  5123. addCopy(titleNode, { title: "复制标题" });
  5124. addCopy(codeNode, { title: "复制番号" });
  5125. GM_addStyle("tbody a[data-copy]{display:inline!important}");
  5126. infoNode.addEventListener("contextmenu", e => {
  5127. const { target } = e;
  5128. if (!target.closest("#x-star") || !target.matches("a")) return;
  5129. handleCopy(e, "", target.textContent);
  5130. });
  5131. }
  5132.  
  5133. this.movieRes();
  5134. this.movieTitle();
  5135. this._movieJump();
  5136. this.movieScore();
  5137. this.movieStar();
  5138. this.driveMatch();
  5139.  
  5140. const tableObs = new MutationObserver((_, obs) => {
  5141. obs.disconnect();
  5142. this.refactorTable();
  5143. });
  5144. tableObs.observe(DOC.querySelector("#movie-loading"), { attributeFilter: ["style"] });
  5145. },
  5146. modifyItem() {
  5147. const container = DOC.querySelector("#related-waterfall");
  5148. if (!container) return;
  5149.  
  5150. const res = [];
  5151. for (const item of container.querySelectorAll(".movie-box")) {
  5152. const code = item.href?.split("/").at(-1);
  5153. if (!CODE_REGEX.test(code)) continue;
  5154.  
  5155. item.append(DOC.create("date", { class: "x-hide" }, code));
  5156. item.querySelector(".photo-info span").classList.add("x-title");
  5157. res.push({ item, code });
  5158. }
  5159. this.driveMatchForList(res, ".photo-frame");
  5160. this.globalClick();
  5161. },
  5162. movieRes() {
  5163. const { res } = this.params;
  5164. if (!res?.length) return;
  5165.  
  5166. unsafeWindow.$(".bigImage").magnificPopup({
  5167. type: "image",
  5168. closeOnContentClick: true,
  5169. closeBtnInside: false,
  5170. image: { verticalFit: false },
  5171. });
  5172. const box = DOC.querySelector(".bigImage");
  5173. const info = DOC.querySelector(".info");
  5174. const first = "x-cover";
  5175. const firstQuery = `a[for=${first}]`;
  5176.  
  5177. const cover = box.querySelector("img");
  5178. cover.setAttribute("id", first);
  5179. cover.classList.add("active");
  5180. box.insertAdjacentHTML(
  5181. "afterbegin",
  5182. '<div class="x-side prev"><span class="glyphicon glyphicon-chevron-left"></div><div class="x-side next"><span class="glyphicon glyphicon-chevron-right"></div>'
  5183. );
  5184. info.insertAdjacentHTML(
  5185. "afterbegin",
  5186. `<div id="x-res" class="btn-group btn-group-sm btn-group-justified mb10">
  5187. <a href="${VOID}" class="btn btn-default active" role="button" for="${first}">封面</a>
  5188. ${res
  5189. .map(
  5190. item =>
  5191. `<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>`
  5192. )
  5193. .join("")}
  5194. </div>`
  5195. );
  5196.  
  5197. box.addEventListener(
  5198. "click",
  5199. e => {
  5200. const { target } = e;
  5201.  
  5202. const side = target.closest(".x-side");
  5203. if (side) {
  5204. e.stopPropagation();
  5205. e.preventDefault();
  5206.  
  5207. const navs = info.querySelectorAll("#x-res a:not([disabled])");
  5208. const { length } = navs;
  5209. if (length === 1) return;
  5210.  
  5211. let index = Array.from(navs).findIndex(item => item.classList.contains("active"));
  5212. index = side.classList.contains("next") ? index + 1 : index - 1;
  5213. if (index > length - 1) index = 0;
  5214. if (index === -1) index = length - 1;
  5215. return navs[index].click();
  5216. }
  5217.  
  5218. if (target.closest(".x-player")) {
  5219. e.stopPropagation();
  5220. e.preventDefault();
  5221. return box.querySelector(".x-side.next").click();
  5222. }
  5223.  
  5224. const { nodeName } = target;
  5225. if (nodeName === "IMG") box.href = target.src;
  5226. if (nodeName !== "VIDEO") return;
  5227.  
  5228. e.stopPropagation();
  5229. e.preventDefault();
  5230. target.paused ? target.play() : target.pause();
  5231. },
  5232. true
  5233. );
  5234. info.querySelector("#x-res").addEventListener("click", ({ target }) => {
  5235. const tag = target.getAttribute("for");
  5236. if (!tag) return;
  5237.  
  5238. const active = box.querySelector(`#${tag}`);
  5239. if (target.classList.contains("active")) return active.focus();
  5240.  
  5241. const _target = info.querySelector("#x-res .active");
  5242. _target.classList.toggle("active");
  5243. if (_target.matches(firstQuery)) box.classList.remove("x-player");
  5244.  
  5245. target.classList.toggle("active");
  5246. if (target.matches(firstQuery) && box.classList.contains("isPlayer")) {
  5247. box.classList.add("x-player");
  5248. }
  5249.  
  5250. const _active = box.querySelector(".active");
  5251. _active.classList.toggle("active");
  5252. if (_active.nodeName === "VIDEO") _active.pause();
  5253.  
  5254. active.classList.toggle("active");
  5255. if (active.nodeName !== "VIDEO") return;
  5256.  
  5257. active.focus();
  5258. active.play();
  5259. });
  5260. res.forEach(key => {
  5261. this.upMovieResource(key, val => {
  5262. const id = `x-${key}`;
  5263. const nav = info.querySelector(`a[for=${id}]`);
  5264. if (!val.length) {
  5265. nav.innerHTML = "暂无";
  5266. return;
  5267. }
  5268.  
  5269. this.info[key] = val;
  5270. nav.removeAttribute("disabled");
  5271. nav.innerHTML = TAB_NAME[key];
  5272.  
  5273. const node = key === "img" ? DOC.create(key, { src: val, id }) : createVideo(val, { id });
  5274. box.append(node);
  5275. if (key === "img" || !nav.previousElementSibling?.matches(firstQuery)) return;
  5276. box.classList.add("isPlayer", "x-player");
  5277. });
  5278. });
  5279. },
  5280. movieTitle() {
  5281. this.upMovieResource(
  5282. "title",
  5283. val => {
  5284. DOC.querySelector("#x-title").textContent = val.length ? val : "暂无数据";
  5285. },
  5286. () => {
  5287. DOC.querySelector("span[style='color:#CC0000;']").parentElement.insertAdjacentHTML(
  5288. "beforebegin",
  5289. '<p><span class="header">机翻标题:</span> <span id="x-title">查询中...</span></p>'
  5290. );
  5291. }
  5292. );
  5293. },
  5294. _movieJump() {
  5295. this.movieJump(
  5296. this.info,
  5297. res => {
  5298. DOC.querySelector("span[style='color:#CC0000;']").parentElement.insertAdjacentHTML(
  5299. "afterend",
  5300. res
  5301. .map(
  5302. ({ group, list }) =>
  5303. `<p><span class="header">${group}:</span> ${list
  5304. .map(
  5305. ({ url, query, name }) =>
  5306. `<button type="button" class="x-jump btn ${
  5307. query ? "btn-default" : "btn-link"
  5308. } btn-xs" data-query="${query}" data-href="${url}">${name}</button>`
  5309. )
  5310. .join("")}</p>`
  5311. )
  5312. .join("")
  5313. );
  5314. },
  5315. (res, node) => node.classList.replace("btn-default", res.zh ? "btn-warning" : "btn-primary")
  5316. );
  5317. },
  5318. movieScore() {
  5319. const { score } = this.params;
  5320. if (!score?.length) return;
  5321.  
  5322. DOC.querySelector("p.header, p.star-show").insertAdjacentHTML(
  5323. "beforebegin",
  5324. score
  5325. .map(
  5326. item =>
  5327. `<p><span class="header">${item.toUpperCase()}评分:</span> <span id="x-${item}_score">查询中...</span></p>`
  5328. )
  5329. .join("")
  5330. );
  5331. score.forEach(key => {
  5332. key = `${key}_score`;
  5333. this.upMovieResource(key, val => {
  5334. const node = DOC.querySelector(`#x-${key}`);
  5335.  
  5336. if (!val.length) {
  5337. node.textContent = "暂无数据";
  5338. return;
  5339. }
  5340.  
  5341. const { score, total, num } = val[0];
  5342. let stars = Math.floor((score / total) * 5);
  5343. stars = Array(5).fill(" x-star", 0, stars).fill("", stars, 5);
  5344.  
  5345. node.innerHTML = `${stars
  5346. .map(item => `<span class="glyphicon glyphicon-star${item}"></span>`)
  5347. .join("")} ${score}分${num ? `, ${num}人评价` : ""}`;
  5348. });
  5349. });
  5350. },
  5351. movieStar() {
  5352. const noStar = DOC.querySelector(".glyphicon-info-sign");
  5353. if (!noStar) return DOC.querySelector(".info ul + p").setAttribute("id", "x-star");
  5354. noStar.nextSibling.replaceWith(DOC.create("p", { id: "x-star" }, "暫無出演者資訊"));
  5355.  
  5356. this.upMovieResource(
  5357. "star",
  5358. val => {
  5359. this.info.star = val;
  5360. DOC.querySelector("#x-star").innerHTML = !val.length
  5361. ? "暂无数据"
  5362. : val
  5363. .map(item => `<span class="genre"><a href="/searchstar/${item}">${item}</a></span>`)
  5364. .join("");
  5365. },
  5366. () => {
  5367. DOC.querySelector("#x-star").innerHTML = "查询中...";
  5368. }
  5369. );
  5370. },
  5371. refactorTable() {
  5372. const table = DOC.querySelector("#magnet-table");
  5373. const keys = (this.params?.magnet ?? []).map(item => `${item}_magnet`);
  5374. const magnets = [];
  5375.  
  5376. if (this.params.hasOwnProperty("sub")) {
  5377. keys.unshift("sub");
  5378. } else {
  5379. for (const tr of table.querySelectorAll("tr")) {
  5380. let [first, size, date] = tr.querySelectorAll("td");
  5381. if (!first || !size || !date) continue;
  5382.  
  5383. const link = first.querySelector("a");
  5384. if (!link?.href) continue;
  5385.  
  5386. size = size?.textContent?.trim() ?? "";
  5387. magnets.push({
  5388. name: link?.textContent?.trim() ?? "",
  5389. link: link.href.split("&")[0],
  5390. zh: !!first.querySelector("a.btn.btn-mini-new.btn-warning.disabled"),
  5391. size,
  5392. bytes: transToBytes(size),
  5393. date: date?.textContent?.trim() ?? "",
  5394. });
  5395. }
  5396. }
  5397. table.parentElement.innerHTML = `
  5398. <table class="table table-striped table-hover table-bordered x-table">
  5399. <caption><div class="x-flex-center x-caption">重构的表格</div></caption>
  5400. <thead>
  5401. <tr>
  5402. <th scope="col">#</th>
  5403. <th scope="col">磁力名称</th>
  5404. <th scope="col">档案大小</th>
  5405. <th scope="col">分享日期</th>
  5406. <th scope="col" class="text-center">来源</th>
  5407. <th scope="col" class="text-center">字幕</th>
  5408. <th scope="col">操作</th>
  5409. </tr>
  5410. </thead>
  5411. <tbody>
  5412. <tr><th scope="row" colspan="7" class="text-center text-muted">暂无数据</th></tr>
  5413. </tbody>
  5414. <tfoot>
  5415. <tr>
  5416. <th scope="row"></th>
  5417. <th></th>
  5418. <th colspan="4" class="text-right">总数</th>
  5419. <td>0</td>
  5420. </tr>
  5421. </tfoot>
  5422. </table>`;
  5423. const caption = DOC.querySelector(".x-table .x-caption");
  5424.  
  5425. this.refactorTbody(magnets);
  5426. keys.forEach(key => {
  5427. this.upMovieResource(
  5428. key,
  5429. res => this.refactorTbody(res, key),
  5430. () => {
  5431. caption.insertAdjacentHTML(
  5432. "beforeend",
  5433. `<span class="label label-default" id="x-${key}"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> ${
  5434. key === "sub" ? "字幕筛选" : `${key.split("_")[0].toUpperCase()}搜索`
  5435. }</span>`
  5436. );
  5437. }
  5438. );
  5439. });
  5440. },
  5441. refactorTbody(magnets, key) {
  5442. if (!magnets.length) return;
  5443.  
  5444. const table = DOC.querySelector(".x-table");
  5445. if (key) table.querySelector(`#x-${key}`).classList.replace("label-default", "label-success");
  5446. let start;
  5447.  
  5448. if (this.magnets.length) {
  5449. for (const item of magnets) {
  5450. const { link, zh } = item;
  5451. const index = this.magnets.findIndex(item => item.link.toLowerCase() === link.toLowerCase());
  5452. if (index === -1) {
  5453. this.magnets.push(item);
  5454. continue;
  5455. }
  5456. if (zh) this.magnets[index].zh = zh;
  5457. }
  5458. magnets = this.magnets;
  5459. } else {
  5460. start = () => {
  5461. table
  5462. .querySelector(".x-caption")
  5463. .insertAdjacentHTML(
  5464. "beforeend",
  5465. '<span class="label label-success" id="x-sort"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> 磁力排序</span>'
  5466. );
  5467. };
  5468.  
  5469. if (this.M_COPY) {
  5470. table.querySelector(
  5471. "thead th:last-child"
  5472. ).innerHTML = `<a href="${VOID}" data-copy="all" title="复制全部磁链">复制全部</a>`;
  5473. }
  5474.  
  5475. table.addEventListener("click", e => {
  5476. const { copy, magnet } = e.target.dataset;
  5477. if (!copy && !magnet) return;
  5478.  
  5479. if (magnet) return this._driveOffLine(e);
  5480. if (copy !== "all") return handleCopy(e);
  5481. handleCopy(e, "", this.magnets.map(item => item.link).join("\n"));
  5482. });
  5483. }
  5484. table.querySelector("tfoot td").textContent = magnets.length;
  5485. magnets = this.movieSort(magnets, start);
  5486. this.magnets = magnets;
  5487. table.querySelector("tbody").innerHTML = this.refactorTr(magnets);
  5488. },
  5489. refactorTr(magnets) {
  5490. return magnets
  5491. .map(
  5492. ({ name, link, size, date, from, href, zh }, index) => `
  5493. <tr>
  5494. <th scope="row">${++index}</th>
  5495. <th class="x-line" title="${name}">
  5496. <a href="${link}">${name}</a>
  5497. </th>
  5498. <td>${size}</td>
  5499. <td>${date}</td>
  5500. <td class="text-center">
  5501. <a href="${href || VOID}"${href ? ` target="_blank" title="查看详情"` : ""}>
  5502. <code>${from || Domain}</code>
  5503. </a>
  5504. </td>
  5505. <td class="text-center">
  5506. <span class="glyphicon glyphicon-${zh ? "ok" : "remove"}-circle text-${
  5507. zh ? "success" : "danger"
  5508. }"></span>
  5509. </td>
  5510. <td>
  5511. <a hidden href="${VOID}" data-copy="${link}" title="复制磁力链接">复制磁链</a><a hidden href="${VOID}" data-magnet="${link}" class="text-success" title="添加离线任务">添加离线</a>
  5512. </td>
  5513. </tr>`
  5514. )
  5515. .join("");
  5516. },
  5517. driveMatch() {
  5518. this.driveMatchForMovie(
  5519. this.info,
  5520. res => {
  5521. const refresh = DOC.querySelector("#x-refresh");
  5522. refresh.inert = false;
  5523. refresh.textContent = "刷新资源";
  5524. refresh.classList.remove("active");
  5525.  
  5526. DOC.querySelector("#x-match").innerHTML = !res.length
  5527. ? "暂无数据"
  5528. : res
  5529. .map(
  5530. ({ n, pc, fid, cid, t }) =>
  5531. `<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>`
  5532. )
  5533. .join("");
  5534. },
  5535. () => {
  5536. const refresh = DOC.querySelector("#x-refresh");
  5537. if (refresh) {
  5538. refresh.inert = true;
  5539. refresh.textContent = "请求中...";
  5540. return refresh.classList.add("active");
  5541. }
  5542.  
  5543. const { icon, resources } = GM_info.script;
  5544. addPrefetch([icon, ...resources.map(item => item.url)]);
  5545. GM_addStyle("tbody a[data-magnet]{display:inline!important}");
  5546. DOC.querySelector(".info").insertAdjacentHTML(
  5547. "beforeend",
  5548. `<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>`
  5549. );
  5550. DOC.querySelector("#x-refresh").addEventListener("click", () => this.driveMatch());
  5551. DOC.querySelector("#x-magnet").addEventListener("click", e => this._driveOffLine(e));
  5552. const modify = this.upModifyTarget();
  5553. DOC.querySelector("#x-match").addEventListener("click", e => {
  5554. if (e.target.dataset.pc) this.advancedLink(e, { type: "left" });
  5555. if (e.target.classList.contains("x-btn")) this.driveModify(e, modify);
  5556. });
  5557. DOC.querySelector("#x-match").addEventListener("contextmenu", e => {
  5558. if (e.target.dataset.cid) this.advancedLink(e, { type: "right" });
  5559. });
  5560. }
  5561. );
  5562. },
  5563. _driveOffLine(e) {
  5564. this.driveOffLine(e, { ...this.info, magnets: this.magnets });
  5565. },
  5566. };
  5567. }
  5568. class Drive115 {
  5569. beforeUnload_time = 0;
  5570. docStart() {
  5571. Store.setVerifyStatus("pending");
  5572.  
  5573. unsafeWindow.onbeforeunload = () => {
  5574. this.beforeUnload_time = new Date().getTime();
  5575. };
  5576. unsafeWindow.onunload = () => {
  5577. if (new Date().getTime() - this.beforeUnload_time > 5) return;
  5578. Store.setVerifyStatus("failed");
  5579. };
  5580. }
  5581. contentLoaded() {
  5582. unsafeWindow.focus();
  5583. DOC.querySelector("#js_ver_code_box button[rel=verify]").addEventListener("click", () => {
  5584. const interval = setInterval(() => {
  5585. if (DOC.querySelector(".vcode-hint").getAttribute("style").indexOf("none") !== -1) {
  5586. Store.setVerifyStatus("verified");
  5587. unsafeWindow.onbeforeunload = null;
  5588. unsafeWindow.onunload = null;
  5589. clearTimer();
  5590. unsafeWindow.open("", "_self");
  5591. unsafeWindow.close();
  5592. }
  5593. }, 300);
  5594. const timeout = setTimeout(() => clearTimer(), 600);
  5595.  
  5596. const clearTimer = () => {
  5597. clearInterval(interval);
  5598. clearTimeout(timeout);
  5599. };
  5600. });
  5601. }
  5602. }
  5603.  
  5604. try {
  5605. const Process = eval(`new ${Domain}()`);
  5606. Process.docStart?.();
  5607. DOC.addEventListener("XContentLoaded", () => Process.contentLoaded?.(), { once: true });
  5608. } catch (err) {
  5609. console.error(`${GM_info.script.name}: 无匹配模块`);
  5610. }
  5611. })();