JavScript

一站式体验,JavBus & JavDB 兼容

À partir de 2022-07-04. Voir la dernière version.

  1. // ==UserScript==
  2. // @name JavScript
  3. // @namespace JavScript@blc
  4. // @version 3.4.6
  5. // @author blc
  6. // @description 一站式体验,JavBus & JavDB 兼容
  7. // @icon https://s1.ax1x.com/2022/04/01/q5lzYn.png
  8. // @include *
  9. // @require https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js
  10. // @require https://unpkg.com/infinite-scroll@4/dist/infinite-scroll.pkgd.min.js
  11. // @resource play https://s4.ax1x.com/2022/01/12/7nYuKe.png
  12. // @resource success https://s1.ax1x.com/2022/04/01/q5l2LD.png
  13. // @resource info https://s1.ax1x.com/2022/04/01/q5lyz6.png
  14. // @resource warn https://s1.ax1x.com/2022/04/01/q5lgsO.png
  15. // @resource error https://s1.ax1x.com/2022/04/01/q5lcQK.png
  16. // @supportURL https://t.me/+bAWrOoIqs3xmMjll
  17. // @connect *
  18. // @run-at document-start
  19. // @grant GM_addValueChangeListener
  20. // @grant GM_registerMenuCommand
  21. // @grant GM_getResourceURL
  22. // @grant GM_xmlhttpRequest
  23. // @grant GM_setClipboard
  24. // @grant GM_notification
  25. // @grant GM_deleteValue
  26. // @grant GM_listValues
  27. // @grant GM_addElement
  28. // @grant GM_openInTab
  29. // @grant GM_addStyle
  30. // @grant GM_setValue
  31. // @grant GM_getValue
  32. // @grant GM_info
  33. // @license GPL-3.0-only
  34. // @compatible chrome ≥ 88 & Tampermonkey
  35. // @compatible edge ≥ 88 & Tampermonkey
  36. // ==/UserScript==
  37.  
  38. /**
  39. * TODO:
  40. * ⏳ 网盘 - 一键离线 离线结果弹窗逻辑调整
  41. * ⏳ 网盘 - 资源重命名 逻辑优化
  42. * ❓ 网盘 - 离线垃圾文件清理
  43. * ❓ 网盘 - 一键离线 自动/手动
  44. * ❓ 列表 - 自定义数据聚合页
  45. */
  46.  
  47. (function () {
  48. // match
  49. const MatchDomains = [
  50. { domain: "JavBus", regex: /(jav|bus|dmm|see|cdn|fan){2}\./g },
  51. { domain: "JavDB", regex: /javdb\d*\.com/g },
  52. { domain: "Drive115", regex: /captchaapi\.115\.com/g },
  53. ];
  54. const Domain = MatchDomains.find(({ regex }) => regex.test(location.host))?.domain;
  55. if (!Domain) return;
  56.  
  57. // document
  58. const DOC = document;
  59. DOC.create = (tag, attrs = {}, child) => {
  60. const element = DOC.createElement(tag);
  61. Object.keys(attrs).forEach(name => element.setAttribute(name, attrs[name]));
  62. typeof child === "string" && element.appendChild(DOC.createTextNode(child));
  63. typeof child === "object" && element.appendChild(child);
  64. return element;
  65. };
  66.  
  67. // request
  68. const request = (url, data = {}, method = "GET", params = {}) => {
  69. method = method ? method.toUpperCase().trim() : "GET";
  70. if (!url || !["GET", "POST"].includes(method)) return;
  71.  
  72. if (Object.prototype.toString.call(data) === "[object Object]") {
  73. data = Object.keys(data).reduce((pre, cur) => {
  74. return `${pre ? `${pre}&` : pre}${cur}=${encodeURIComponent(data[cur])}`;
  75. }, "");
  76. }
  77. if (method === "GET") {
  78. params.responseType = params.responseType ?? "document";
  79. if (data) {
  80. if (url.includes("?")) {
  81. url = `${url}${url.endsWith("&") ? "" : "&"}${data}`;
  82. } else {
  83. url = `${url}?${data}`;
  84. }
  85. }
  86. }
  87. if (method === "POST") {
  88. params.responseType = params.responseType ?? "json";
  89. const headers = params.headers ?? {};
  90. params.headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", ...headers };
  91. }
  92. // const headers = params.headers ?? {};
  93. // params.headers = { Referer: "", "Cache-Control": "no-cache", ...headers };
  94.  
  95. return new Promise(resolve => {
  96. GM_xmlhttpRequest({
  97. url,
  98. data,
  99. method,
  100. timeout: 20000,
  101. onload: ({ status, response }) => {
  102. if (response?.errcode === 911) verify();
  103. if (status === 404) response = false;
  104. if (response && ["", "text"].includes(params.responseType)) {
  105. if (/<\/?[a-z][\s\S]*>/i.test(response)) {
  106. response = new DOMParser().parseFromString(response, "text/html");
  107. } else if (/^{.*}$/.test(response)) {
  108. response = JSON.parse(response);
  109. }
  110. }
  111. resolve(response);
  112. },
  113. ...params,
  114. });
  115. });
  116. };
  117.  
  118. // utils
  119. const getDate = (timestamp, separator = "-") => {
  120. const date = timestamp ? new Date(timestamp) : new Date();
  121. const Y = date.getFullYear();
  122. const M = `${date.getMonth() + 1}`.padStart(2, "0");
  123. const D = `${date.getDate()}`.padStart(2, "0");
  124. return `${Y}${separator}${M}${separator}${D}`;
  125. };
  126. const addCopyTarget = (selectors, attrs = {}) => {
  127. const node = DOC.querySelector(selectors);
  128. const _attrs = { "data-copy": node?.textContent ?? "", class: "x-ml", href: "javascript:void(0);" };
  129. const target = DOC.create("a", { ..._attrs, ...attrs }, "复制");
  130. target.addEventListener("click", handleCopyTxt);
  131. node.appendChild(target);
  132. };
  133. const handleCopyTxt = (e, tip = "成功") => {
  134. const { target } = e;
  135. const copy = target?.dataset?.copy?.trim() ?? "";
  136. if (!copy) return;
  137.  
  138. e.preventDefault();
  139. e.stopPropagation();
  140.  
  141. GM_setClipboard(copy);
  142. const { textContent = "" } = target;
  143. target.textContent = tip;
  144.  
  145. const timer = setTimeout(() => {
  146. target.textContent = textContent;
  147. clearTimeout(timer);
  148. }, 400);
  149.  
  150. return 1;
  151. };
  152. const transToBytes = sizeStr => {
  153. const sizeNum = sizeStr.replace(/[a-zA-Z\s]/g, "");
  154. if (sizeNum <= 0) return 0;
  155.  
  156. const matchList = [
  157. { unit: /byte/gi, transform: size => size },
  158. { unit: /kb/gi, transform: size => size * 1000 },
  159. { unit: /mb/gi, transform: size => size * Math.pow(1000, 2) },
  160. { unit: /gb/gi, transform: size => size * Math.pow(1000, 3) },
  161. { unit: /kib/gi, transform: size => size * 1024 },
  162. { unit: /mib/gi, transform: size => size * Math.pow(1024, 2) },
  163. { unit: /gib/gi, transform: size => size * Math.pow(1024, 3) },
  164. ];
  165.  
  166. return (
  167. matchList
  168. .find(({ unit }) => unit.test(sizeStr))
  169. ?.transform(sizeNum)
  170. ?.toFixed(2) ?? 0
  171. );
  172. };
  173. const unique = (arr, key) => {
  174. if (!key) return Array.from(new Set(arr));
  175.  
  176. arr = arr.map(item => {
  177. item[key] = item[key]?.toLowerCase();
  178. return item;
  179. });
  180. return Array.from(new Set(arr.map(e => e[key]))).map(e => arr.find(x => x[key] === e));
  181. };
  182. const openInTab = (url, active = true, params = {}) => {
  183. GM_openInTab(url, { active: !!active, insert: true, setParent: true, incognito: false, ...params });
  184. };
  185. const notify = msg => {
  186. GM_notification({
  187. highlight: true,
  188. silent: true,
  189. timeout: 3000,
  190. ...msg,
  191. text: msg?.text || GM_info.script.name,
  192. image: GM_getResourceURL(msg?.image ?? "info"),
  193. onclick: msg?.clickUrl ? () => openInTab(msg.clickUrl) : () => {},
  194. });
  195. };
  196. const verify = () => {
  197. const h = 667;
  198. const w = 375;
  199. const t = (window.screen.availHeight - h) / 2;
  200. const l = (window.screen.availWidth - w) / 2;
  201.  
  202. window.open(
  203. `https://captchaapi.115.com/?ac=security_code&type=web&cb=Close911_${new Date().getTime()}`,
  204. "验证账号",
  205. `height=${h},width=${w},top=${t},left=${l},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
  206. );
  207. };
  208. const delay = n => new Promise(r => setTimeout(r, n * 1000));
  209. const codeParse = code => {
  210. const codes = code
  211. .split(/-|_/)
  212. .filter(Boolean)
  213. .map(item => (item.startsWith("0") ? item.slice(1) : item));
  214.  
  215. return {
  216. prefix: codes[0],
  217. regex: new RegExp(codes.join("(0|-|_){0,4}"), "i"),
  218. };
  219. };
  220. const fadeInImg = nodeList => {
  221. const loaded = node => node.classList.add("x-in");
  222. nodeList.forEach(node => {
  223. const img = node.querySelector("img");
  224. if (img) img.onload = () => loaded(img);
  225. });
  226. };
  227.  
  228. // store
  229. class Store {
  230. static init() {
  231. const date = getDate();
  232. const cdKey = "CD";
  233. if (GM_getValue(cdKey, "") === date) return;
  234.  
  235. GM_setValue(cdKey, date);
  236. GM_setValue("DETAILS", {});
  237. GM_setValue("RESOURCE", []);
  238. GM_setValue("TEMPORARY_OBS", []);
  239. }
  240. static getDetail(key) {
  241. const details = GM_getValue("DETAILS", {});
  242. return details[key] ?? {};
  243. }
  244. static upDetail(key, val = {}) {
  245. const details = GM_getValue("DETAILS", {});
  246. details[key] = { ...(details[key] ?? {}), ...val };
  247. GM_setValue("DETAILS", details);
  248. }
  249. static addTemporaryOb(val) {
  250. if (!val) return;
  251. const obs = GM_getValue("TEMPORARY_OBS", []);
  252. obs.push(val);
  253. GM_setValue("TEMPORARY_OBS", obs);
  254. }
  255. static reduceTemporaryOb(val) {
  256. if (!val) return;
  257. const obs = GM_getValue("TEMPORARY_OBS", []).filter(item => item !== val);
  258. GM_setValue("TEMPORARY_OBS", obs);
  259. }
  260. }
  261.  
  262. // apis
  263. class Apis {
  264. // movie
  265. static async movieImg(code) {
  266. code = code.toUpperCase();
  267. const { regex } = codeParse(code);
  268.  
  269. let [blogJav, javStore] = await Promise.all([
  270. request(`https://www.google.com/search?q=${code} site:blogjav.net`),
  271. request(`https://javstore.net/search/${code}.html`),
  272. ]);
  273. blogJav = Array.from(blogJav?.querySelectorAll("#rso .g .yuRUbf a") ?? []).find(item => {
  274. return regex.test(item?.querySelector("h3")?.textContent ?? "");
  275. });
  276. javStore = Array.from(javStore?.querySelectorAll("#content_news li a") ?? []).find(item => {
  277. return regex.test(item?.title ?? "");
  278. });
  279.  
  280. const [bjRes, jsRes] = await Promise.all([
  281. request(
  282. `${blogJav?.href ? `http://webcache.googleusercontent.com/search?q=cache:${blogJav.href}` : ""}`
  283. ),
  284. request(javStore?.href ?? ""),
  285. ]);
  286. const bjImg = bjRes
  287. ? bjRes
  288. ?.querySelector("#page .entry-content a img")
  289. ?.getAttribute("data-lazy-src")
  290. .replace("//t", "//img")
  291. .replace("thumbs", "images")
  292. : "";
  293. const jsImg = jsRes ? jsRes?.querySelector(".news a img[alt*='.th']")?.src.replace(".th", "") : "";
  294.  
  295. return bjImg || jsImg || "";
  296. }
  297. static async movieVideo(code, studio) {
  298. code = code.toLowerCase();
  299. const { regex } = codeParse(code);
  300.  
  301. if (studio) {
  302. const matchList = [
  303. {
  304. name: "東京熱",
  305. match: "https://my.cdn.tokyo-hot.com/media/samples/%s.mp4",
  306. },
  307. {
  308. name: "カリビアンコム",
  309. match: "https://smovie.caribbeancom.com/sample/movies/%s/1080p.mp4",
  310. },
  311. {
  312. name: "一本道",
  313. match: "http://smovie.1pondo.tv/sample/movies/%s/1080p.mp4",
  314. },
  315. {
  316. name: "HEYZO",
  317. trans: code => code.replace(/HEYZO\-/gi, ""),
  318. match: "https://www.heyzo.com/contents/3000/%s/heyzo_hd_%s_sample.mp4",
  319. },
  320. ];
  321. const matched = matchList.find(({ name }) => name === studio);
  322. if (matched) return matched.match.replace(/%s/g, matched.trans ? matched.trans(code) : code);
  323. }
  324.  
  325. let [r18, xrmoo] = await Promise.all([
  326. request(`https://www.r18.com/common/search/order=match/searchword=${code}/`),
  327. request(`http://dmm.xrmoo.com/sindex.php?searchstr=${code}`),
  328. ]);
  329. r18 = Array.from(r18?.querySelectorAll("a.js-view-sample") ?? []).find(item => {
  330. return regex.test(item?.dataset?.id ?? "");
  331. });
  332. xrmoo = xrmoo?.querySelector(".card .card-footer a.viewVideo")?.getAttribute("data-link");
  333.  
  334. const type = "video/mp4";
  335. let res = [];
  336.  
  337. if (r18) {
  338. const { videoHigh, videoMed, videoLow } = r18.dataset;
  339. if (videoHigh) res.push({ src: videoHigh, title: "720p", type });
  340. if (videoMed) res.push({ src: videoMed, title: "480p", type });
  341. if (videoLow) res.push({ src: videoLow, title: "360p", type });
  342. res = res.map(item => {
  343. return { ...item, src: item.src.replace("awscc3001.r18.com", "cc3001.dmm.co.jp") };
  344. });
  345. }
  346. if (xrmoo) {
  347. res.push({ src: xrmoo.replace("_sm_w", "_dmb_w"), title: "720p", type });
  348. res.push({ src: xrmoo.replace("_sm_w", "_dm_w"), title: "480p", type });
  349. res.push({ src: xrmoo, title: "360p", type });
  350. }
  351.  
  352. if (res.length) return res.sort((cur, next) => parseInt(next.title, 10) - parseInt(cur.title, 10));
  353. }
  354. static async moviePlayer(code) {
  355. code = code.toUpperCase();
  356. const { regex } = codeParse(code);
  357. const site = "https://netflav.com";
  358.  
  359. let netflav = await request(`${site}/search?type=title&keyword=${code}`);
  360. netflav = Array.from(netflav?.querySelectorAll(".grid_root .grid_cell") ?? []).find(item => {
  361. return regex.test(item?.querySelector(".grid_title")?.textContent ?? "");
  362. });
  363. netflav = netflav?.querySelector("a")?.getAttribute("href");
  364. if (!netflav) return;
  365.  
  366. netflav = await request(`${site}${netflav}`);
  367. netflav = netflav?.querySelector("script#__NEXT_DATA__")?.textContent ?? "{}";
  368. netflav = JSON.parse(netflav)?.props?.initialState?.video?.data?.srcs ?? [];
  369. if (!netflav?.length) return;
  370.  
  371. const matchList = [
  372. {
  373. regex: /\/\/(mm9842\.com|www\.avple\.video|asianclub\.tv)/,
  374. parse: async url => {
  375. const [protocol, href] = url.split("//");
  376. const [host, ...pathname] = href.split("/");
  377.  
  378. const res = await request(
  379. `${protocol}//${host}/api/source/${pathname.pop()}`,
  380. { r: "", d: host },
  381. "POST"
  382. );
  383.  
  384. if (!res?.success) return [];
  385. return (res?.data ?? []).map(({ file, label, type }) => {
  386. return { src: file, title: label, type: `video/${type}` };
  387. });
  388. },
  389. },
  390. {
  391. regex: /\/\/(embedgram\.com|vidoza\.net)/,
  392. parse: async url => {
  393. const res = await request(url);
  394. return Array.from(res?.querySelectorAll("video source") ?? []).map(
  395. ({ src, title = "", type }) => {
  396. return { src, title, type };
  397. }
  398. );
  399. },
  400. },
  401. ];
  402. netflav = await Promise.all(
  403. netflav
  404. .filter(url => matchList.find(({ regex }) => regex.test(url)))
  405. .map(url => matchList.find(({ regex }) => regex.test(url)).parse(url))
  406. );
  407. netflav = netflav.reduce((pre, cur) => [...pre, ...cur], []);
  408. if (!netflav?.length) return;
  409.  
  410. return netflav.sort((cur, next) => parseInt(next.title || 360, 10) - parseInt(cur.title || 360, 10));
  411. }
  412. static async movieTitle(sentence) {
  413. const st = encodeURIComponent(sentence.trim());
  414. const data = {
  415. async: `translate,sl:auto,tl:zh-CN,st:${st},id:1650701080679,qc:true,ac:false,_id:tw-async-translate,_pms:s,_fmt:pc`,
  416. };
  417.  
  418. const res = await request(
  419. "https://www.google.com/async/translate?vet=12ahUKEwixq63V3Kn3AhUCJUQIHdMJDpkQqDh6BAgCECw..i&ei=CbNjYvGCPYLKkPIP05O4yAk&yv=3",
  420. data,
  421. "POST",
  422. { responseType: "" }
  423. );
  424.  
  425. return res?.querySelector("#tw-answ-target-text")?.textContent ?? "";
  426. }
  427. static async movieStar(code) {
  428. code = code.toUpperCase();
  429. const { regex } = codeParse(code);
  430. const site = "https://javdb.com";
  431.  
  432. let res = await request(`${site}/search?q=${code}&sb=0`);
  433. res = Array.from(res?.querySelectorAll(".movie-list .item a") ?? []).find(item => {
  434. return regex.test(item?.querySelector(".video-title strong")?.textContent ?? "");
  435. });
  436. if (!res) return;
  437.  
  438. res = await request(`${site}${res.getAttribute("href")}`);
  439. res = res?.querySelectorAll(".movie-panel-info > .panel-block");
  440. if (!res?.length) return;
  441.  
  442. res = Array.from(res).find(item => {
  443. return /(演員|Actor\(s\)):/.test(item?.querySelector("strong")?.textContent ?? "");
  444. });
  445. if (!res) return;
  446.  
  447. res = res?.querySelector(".value").textContent.trim();
  448. return res
  449. .split(/\n/)
  450. .filter(item => item.indexOf("♀") !== -1)
  451. .map(item => item.replace("♀", "").trim());
  452. }
  453. static async movieMagnet(code) {
  454. code = code.toUpperCase();
  455.  
  456. const matchList = [
  457. {
  458. site: "Sukebei",
  459. host: "https://sukebei.nyaa.si/",
  460. search: "?f=0&c=0_0&q=%s",
  461. selectors: ".table-responsive table tbody tr",
  462. filter: {
  463. name: e => e?.querySelector("td:nth-child(2) a").textContent,
  464. link: e => e?.querySelector("td:nth-child(3) a:last-child").href,
  465. size: e => e?.querySelector("td:nth-child(4)").textContent,
  466. date: e => e?.querySelector("td:nth-child(5)").textContent.split(" ")[0],
  467. href: e => e?.querySelector("td:nth-child(2) a").getAttribute("href"),
  468. },
  469. },
  470. {
  471. site: "BTSOW",
  472. host: "https://btsow.com/",
  473. search: "search/%s",
  474. selectors: ".data-list .row:not(.hidden-xs)",
  475. filter: {
  476. name: e => e?.querySelector(".file").textContent,
  477. link: e => `magnet:?xt=urn:btih:${e?.querySelector("a").href.split("/").pop()}`,
  478. size: e => e?.querySelector(".size").textContent,
  479. date: e => e?.querySelector(".date").textContent,
  480. href: e => e?.querySelector("a").getAttribute("href"),
  481. },
  482. },
  483. // {
  484. // site: "BTDigg",
  485. // host: "https://btdig.com/",
  486. // search: "search?order=0&q=%s",
  487. // selectors: ".one_result",
  488. // filter: {
  489. // name: e => e?.querySelector(".torrent_name").textContent,
  490. // link: e => e?.querySelector(".torrent_magnet a").href,
  491. // size: e => e?.querySelector(".torrent_size").textContent,
  492. // date: e => e?.querySelector(".torrent_age").textContent,
  493. // href: e => e?.querySelector(".torrent_name a").href,
  494. // },
  495. // },
  496. ];
  497.  
  498. const matched = await Promise.all(
  499. matchList.map(({ host, search }) => request(`${host}${search.replace(/%s/g, code)}`))
  500. );
  501.  
  502. const magnets = [];
  503. for (let index = 0; index < matchList.length; index++) {
  504. let node = matched[index];
  505. if (!node) continue;
  506.  
  507. const { selectors, site, filter, host } = matchList[index];
  508. node = node?.querySelectorAll(selectors);
  509. if (!node?.length) continue;
  510.  
  511. for (const item of node) {
  512. const magnet = { from: site };
  513. Object.keys(filter).forEach(key => {
  514. magnet[key] = filter[key](item)?.trim();
  515. });
  516.  
  517. magnet.bytes = transToBytes(magnet.size);
  518. magnet.zh = /中文/g.test(magnet.name);
  519. magnet.link = magnet.link.split("&")[0];
  520. const { href } = magnet;
  521. if (href && !href.includes("//")) magnet.href = `${host}${href.replace(/^\//, "")}`;
  522.  
  523. magnets.push(magnet);
  524. }
  525. }
  526. return magnets;
  527. }
  528. // drive
  529. static async searchVideo(params = { search_value: "" } | "") {
  530. if (typeof params === "string") params = { search_value: params };
  531. if (!params.search_value.trim()) return [];
  532.  
  533. const res = await request(
  534. "https://webapi.115.com/files/search",
  535. {
  536. offset: 0,
  537. limit: 10000,
  538. date: "",
  539. aid: 1,
  540. cid: 0,
  541. pick_code: "",
  542. type: 4,
  543. source: "",
  544. format: "json",
  545. o: "user_ptime",
  546. asc: 0,
  547. star: "",
  548. suffix: "",
  549. ...params,
  550. },
  551. "GET",
  552. { responseType: "json" }
  553. );
  554.  
  555. return (res.data ?? []).map(({ cid, fid, n, pc, t }) => {
  556. return { cid, fid, n, pc, t };
  557. });
  558. }
  559. static async getFile(params = {}) {
  560. const res = await request(
  561. "https://webapi.115.com/files",
  562. {
  563. aid: 1,
  564. cid: 0,
  565. o: "user_ptime",
  566. asc: 0,
  567. offset: 0,
  568. show_dir: 1,
  569. limit: 115,
  570. code: "",
  571. scid: "",
  572. snap: 0,
  573. natsort: 1,
  574. record_open_time: 1,
  575. source: "",
  576. format: "json",
  577. ...params,
  578. },
  579. "GET",
  580. { responseType: "json" }
  581. );
  582.  
  583. return res?.data ?? [];
  584. }
  585. static async getSign() {
  586. const { state, sign, time } = await request(
  587. "http://115.com/",
  588. { ct: "offline", ac: "space", _: new Date().getTime() },
  589. "GET",
  590. { responseType: "json" }
  591. );
  592.  
  593. if (state) return { sign, time };
  594.  
  595. notify({
  596. title: "请求失败,115未登录",
  597. text: "点击跳转登录",
  598. image: "error",
  599. clickUrl: "http://115.com/?mode=login",
  600. timeout: 3000,
  601. });
  602. }
  603. static async addTaskUrl({ url, wp_path_id = "", sign, time }) {
  604. const _sign = sign && time ? { sign, time } : await this.getSign();
  605. if (!_sign) return;
  606.  
  607. return await request(
  608. "https://115.com/web/lixian/?ct=lixian&ac=add_task_url",
  609. { url, wp_path_id, ..._sign },
  610. "POST"
  611. );
  612. }
  613. static getVideo(cid = "") {
  614. return this.getFile({ cid, custom_order: 0, star: "", suffix: "", type: 4 });
  615. }
  616. static driveRename(res) {
  617. const data = {};
  618. for (const { fid, file_name } of res) data[`files_new_name[${fid}]`] = file_name;
  619.  
  620. return request("https://webapi.115.com/files/batch_rename", data, "POST");
  621. }
  622. }
  623.  
  624. // common
  625. class Common {
  626. menus = {
  627. tabs: [
  628. { title: "全站", key: "global", prefix: "G" },
  629. { title: "列表页", key: "list", prefix: "L" },
  630. { title: "详情页", key: "movie", prefix: "M" },
  631. { title: "115 相关", key: "drive", prefix: "D" },
  632. ],
  633. commands: [
  634. "G_DARK",
  635. "G_SEARCH",
  636. "G_CLICK",
  637. "L_MIT",
  638. "L_MTH",
  639. "L_MTL",
  640. "L_SCROLL",
  641. "M_IMG",
  642. "M_VIDEO",
  643. "M_PLAYER",
  644. "M_TITLE",
  645. "M_STAR",
  646. "M_JUMP",
  647. "M_SUB",
  648. "M_SORT",
  649. "M_MAGNET",
  650. "D_MATCH",
  651. "D_CID",
  652. "D_VERIFY",
  653. "D_RENAME",
  654. ],
  655. details: [
  656. {
  657. name: "暗黑模式",
  658. key: "G_DARK",
  659. type: "switch",
  660. info: "常用页面暗黑模式",
  661. defaultVal: window.matchMedia("(prefers-color-scheme: dark)").matches,
  662. hotkey: "d",
  663. },
  664. {
  665. name: "快捷搜索",
  666. key: "G_SEARCH",
  667. type: "switch",
  668. info: "<kbd>/</kbd> 获取焦点,<kbd>ctrl</kbd> + <kbd>/</kbd> 快速搜索粘贴板第一项",
  669. defaultVal: true,
  670. hotkey: "k",
  671. },
  672. {
  673. name: "点击事件",
  674. key: "G_CLICK",
  675. type: "switch",
  676. info: "影片/演员 卡片新窗口打开,左键前台,右键后台",
  677. defaultVal: true,
  678. hotkey: "c",
  679. },
  680. {
  681. name: "预览图替换",
  682. key: "L_MIT",
  683. type: "switch",
  684. info: "替换为封面大图",
  685. defaultVal: true,
  686. },
  687. {
  688. name: "标题等高",
  689. key: "L_MTH",
  690. type: "switch",
  691. info: "影片标题强制等高",
  692. defaultVal: true,
  693. },
  694. {
  695. name: "标题最大行",
  696. key: "L_MTL",
  697. type: "number",
  698. info: "影片标题最大显示行数,超出省略。0 不限制 (等高模式下最小有效值 1)",
  699. placeholder: "仅支持整数 ≥ 0",
  700. defaultVal: 2,
  701. },
  702. {
  703. name: "滚动加载",
  704. key: "L_SCROLL",
  705. type: "switch",
  706. info: "滚动加载下一页",
  707. defaultVal: true,
  708. },
  709. {
  710. name: "预览大图",
  711. key: "M_IMG",
  712. type: "switch",
  713. info: `获取自 <a href="https://blogjav.net/" class="link-primary">BlogJav</a>, <a href="https://javstore.net/" class="link-primary">JavStore</a>`,
  714. defaultVal: true,
  715. },
  716. {
  717. name: "预览视频",
  718. key: "M_VIDEO",
  719. type: "switch",
  720. info: `获取自 <a href="https://www.r18.com/" class="link-primary">R18</a>, <a href="http://dmm.xrmoo.com/" class="link-primary">闲人吧</a>`,
  721. defaultVal: true,
  722. },
  723. {
  724. name: "在线播放",
  725. key: "M_PLAYER",
  726. type: "switch",
  727. info: `获取自 <a href="https://netflav.com/" class="link-primary">Netflav</a>`,
  728. defaultVal: true,
  729. },
  730. {
  731. name: "标题机翻",
  732. key: "M_TITLE",
  733. type: "switch",
  734. info: `翻自 <a href="https://google.com/" class="link-primary">Google</a>`,
  735. defaultVal: true,
  736. },
  737. {
  738. name: "演员匹配",
  739. key: "M_STAR",
  740. type: "switch",
  741. info: `如无,获取自 <a href="https://javdb.com/" class="link-primary">JavDB</a>`,
  742. defaultVal: true,
  743. },
  744. {
  745. name: "站点跳转",
  746. key: "M_JUMP",
  747. type: "switch",
  748. info: `在 <a href="https://www.javbus.com/" class="link-primary">JavBus</a> & <a href="https://javdb.com/" class="link-primary">JavDB</a> 间跳转`,
  749. defaultVal: true,
  750. },
  751. {
  752. name: "字幕筛选",
  753. key: "M_SUB",
  754. type: "switch",
  755. info: "针对名称规则为 <code>大写字母</code> + <code>-C</code> 的磁链的额外过滤",
  756. defaultVal: false,
  757. },
  758. {
  759. name: "磁力排序",
  760. key: "M_SORT",
  761. type: "switch",
  762. info: "综合排序 <code>字幕</code> > <code>大小</code> > <code>日期</code>",
  763. defaultVal: true,
  764. },
  765. {
  766. name: "磁力搜索",
  767. key: "M_MAGNET",
  768. type: "switch",
  769. info: `自动去重,获取自 <a href="https://sukebei.nyaa.si/" class="link-primary">Sukebei</a>, <a href="https://btsow.com/" class="link-primary">BTSOW</a>`,
  770. defaultVal: true,
  771. },
  772. {
  773. name: "网盘资源",
  774. key: "D_MATCH",
  775. type: "switch",
  776. info: "资源匹配 & 离线开关 (<strong>请确保网盘已登录</strong>)",
  777. defaultVal: true,
  778. },
  779. {
  780. name: "下载目录",
  781. key: "D_CID",
  782. type: "input",
  783. info: "设置离线下载目录 <strong>cid</strong> 或 <strong>动态参数</strong>,建议 <strong>cid</strong> 效率更高<br><strong>动态参数</strong> 支持网盘 <strong>根目录</strong> 下文件夹名称<br>默认动态参数 <code>${云下载}</code>",
  784. placeholder: "cid 或 动态参数",
  785. defaultVal: "${云下载}",
  786. },
  787. {
  788. name: "离线验证",
  789. key: "D_VERIFY",
  790. type: "number",
  791. info: "『<strong>一键离线</strong>』后执行,查询以验证离线下载结果,每次间隔一秒<br>设置验证次数上限,上限次数越多验证越精准<br>建议 3 ~ 5,默认 3",
  792. placeholder: "仅支持整数 ≥ 0",
  793. defaultVal: 3,
  794. },
  795. {
  796. name: "文件重命名",
  797. key: "D_RENAME",
  798. type: "input",
  799. info: '『<strong>一键离线</strong>』&『<strong>离线验证</strong>』后执行,支持动态参数如下<br><code>${字幕}</code> "【中文字幕】",非字幕资源则为空<br><code>${番号}</code> 页面番号,字母自动转大写 (番号为必须值,如重命名未包含将自动追加前缀)<br><code>${标题}</code> 页面标题,标题可能已包含番号,需自行判断<br><code>${序号}</code> 仅作用于视频文件,数字 1 起',
  800. placeholder: "勿填写后缀,可能导致资源不可用",
  801. defaultVal: "${字幕}${标题}",
  802. },
  803. ],
  804. };
  805. route = null;
  806. pcUrl = "https://v.anxia.com/?pickcode=";
  807.  
  808. listener = {
  809. id: null,
  810. list: [],
  811. };
  812.  
  813. init() {
  814. Store.init();
  815. this.route = Object.keys(this.routes).find(key => this.routes[key].test(location.pathname));
  816. this.createMenu();
  817. return { ...this, ...this[this.route] };
  818. }
  819. createMenu() {
  820. GM_addStyle(`
  821. .x-scrollbar-hide body::-webkit-scrollbar {
  822. display: none;
  823. }
  824. .x-mask {
  825. position: fixed;
  826. top: 0;
  827. left: 0;
  828. z-index: 9999;
  829. display: none;
  830. box-sizing: border-box;
  831. width: 100vw;
  832. height: 100vh;
  833. margin: 0;
  834. padding: 0;
  835. background: transparent;
  836. border: none;
  837. backdrop-filter: blur(50px);
  838. }
  839. iframe.x-mask {
  840. backdrop-filter: none;
  841. }
  842. .x-hide {
  843. display: none;
  844. }
  845. .x-show {
  846. display: block !important;
  847. }
  848. `);
  849.  
  850. let { tabs, commands, details } = this.menus;
  851.  
  852. const exclude = this.excludeMenu ?? [];
  853. if (exclude?.length) {
  854. const regex = new RegExp(`^(?!${exclude.join("|")})`);
  855. commands = commands.filter(command => regex.test(command));
  856. tabs = tabs.filter(tab => commands.find(command => command.startsWith(tab.prefix)));
  857. }
  858. if (!commands.length) return;
  859.  
  860. // const { domain } = Matched;
  861. const active = tabs.find(({ key }) => key === this.route) ?? tabs[0];
  862.  
  863. let tabStr = "";
  864. let panelStr = "";
  865. for (let index = 0; index < tabs.length; index++) {
  866. const { title, key, prefix } = tabs[index];
  867.  
  868. const curCommands = commands.filter(command => command.startsWith(prefix));
  869. const curLen = curCommands.length;
  870. if (!curLen) continue;
  871.  
  872. const isActive = key === active.key;
  873. tabStr += `
  874. <a
  875. class="nav-link${isActive ? " active" : ""}"
  876. id="${key}-tab"
  877. data-bs-toggle="pill"
  878. href="#${key}"
  879. role="tab"
  880. aria-controls="${key}"
  881. aria-selected="${isActive}"
  882. >
  883. ${title}设置
  884. </a>`;
  885. panelStr += `
  886. <div
  887. class="tab-pane fade${isActive ? " show active" : ""}"
  888. id="${key}"
  889. role="tabpanel"
  890. aria-labelledby="${key}-tab"
  891. >
  892. `;
  893.  
  894. for (let curIdx = 0; curIdx < curLen; curIdx++) {
  895. const {
  896. key: curKey,
  897. defaultVal,
  898. name,
  899. type,
  900. hotkey = "",
  901. placeholder = "",
  902. info,
  903. } = details.find(item => item.key === curCommands[curIdx]);
  904.  
  905. const uniKey = `${Domain}_${curKey}`;
  906. const val = GM_getValue(uniKey, defaultVal);
  907. this[curKey] = val;
  908.  
  909. panelStr += `<div${curIdx + 1 === curLen ? "" : ` class="mb-3"`}>`;
  910.  
  911. if (type === "switch") {
  912. if (curKey.startsWith("G")) {
  913. GM_registerMenuCommand(
  914. `${val ? "关闭" : "开启"}${name}`,
  915. () => {
  916. GM_setValue(uniKey, !val);
  917. location.reload();
  918. },
  919. hotkey
  920. );
  921. }
  922. panelStr += `
  923. <div class="form-check form-switch">
  924. <input
  925. type="checkbox"
  926. class="form-check-input"
  927. id="${curKey}"
  928. aria-describedby="${curKey}_Help"
  929. ${val ? "checked" : ""}
  930. name="${curKey}"
  931. >
  932. <label class="form-check-label" for="${curKey}">${name}</label>
  933. </div>
  934. `;
  935. } else {
  936. panelStr += `
  937. <label class="form-label" for="${curKey}">${name}</label>
  938. <input
  939. type="${type}"
  940. class="form-control"
  941. id="${curKey}"
  942. aria-describedby="${curKey}_Help"
  943. value="${val ?? ""}"
  944. placeholder="${placeholder}"
  945. name="${curKey}"
  946. >
  947. `;
  948. }
  949.  
  950. if (info) panelStr += `<div id="${curKey}_Help" class="form-text">${info}</div>`;
  951. panelStr += `</div>`;
  952. }
  953.  
  954. panelStr += `</div>`;
  955. }
  956.  
  957. if (!tabStr || !panelStr) return;
  958. DOC.addEventListener(
  959. "DOMContentLoaded",
  960. () => {
  961. DOC.body.insertAdjacentHTML(
  962. "beforeend",
  963. `<iframe
  964. class="x-mask"
  965. id="control-panel"
  966. name="control-panel"
  967. src="about:blank"
  968. title="控制面板"
  969. ></iframe>`
  970. );
  971. const iframe = DOC.querySelector("iframe#control-panel");
  972. const _DOC = iframe.contentWindow.document;
  973.  
  974. _DOC.querySelector("head").insertAdjacentHTML(
  975. "beforeend",
  976. `<link
  977. href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
  978. rel="stylesheet"
  979. >
  980. <style>${this.style}</style>
  981. <base target="_blank">`
  982. );
  983. const body = _DOC.querySelector("body");
  984. body.classList.add("bg-transparent");
  985. GM_addElement(body, "script", {
  986. src: "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js",
  987. });
  988. body.insertAdjacentHTML(
  989. "afterbegin",
  990. `<button
  991. type="button"
  992. class="d-none"
  993. id="openModal"
  994. class="btn btn-primary"
  995. data-bs-toggle="modal"
  996. data-bs-target="#controlPanel"
  997. >
  998. open
  999. </button>
  1000. <div
  1001. class="modal fade"
  1002. id="controlPanel"
  1003. tabindex="-1"
  1004. aria-labelledby="controlPanelLabel"
  1005. aria-hidden="true"
  1006. >
  1007. <div class="modal-dialog modal-lg modal-fullscreen-lg-down modal-dialog-scrollable">
  1008. <div class="modal-content">
  1009. <div class="modal-header">
  1010. <h5 class="modal-title" id="controlPanelLabel">
  1011. 控制面板
  1012. -
  1013. <a
  1014. href="https://sleazyfork.org/zh-CN/scripts/435360-javscript"
  1015. class="link-secondary text-decoration-none"
  1016. >
  1017. ${GM_info.script.name} v${GM_info.script.version}
  1018. </a>
  1019. </h5>
  1020. <button
  1021. type="button"
  1022. class="btn-close"
  1023. data-bs-dismiss="modal"
  1024. aria-label="Close"
  1025. >
  1026. </button>
  1027. </div>
  1028. <div class="modal-body">
  1029. <form class="mb-0">
  1030. <div class="d-flex align-items-start">
  1031. <div
  1032. class="nav flex-column nav-pills me-3 sticky-top"
  1033. id="v-pills-tab"
  1034. role="tablist"
  1035. aria-orientation="vertical"
  1036. >
  1037. ${tabStr}
  1038. </div>
  1039. <div class="tab-content flex-fill" id="v-pills-tabContent">
  1040. ${panelStr}
  1041. </div>
  1042. </div>
  1043. </form>
  1044. </div>
  1045. <div class="modal-footer">
  1046. <button
  1047. type="button"
  1048. class="btn btn-danger"
  1049. data-bs-dismiss="modal"
  1050. data-action="restart"
  1051. >
  1052. 重置脚本
  1053. </button>
  1054. <button
  1055. type="button"
  1056. class="btn btn-secondary"
  1057. data-bs-dismiss="modal"
  1058. data-action="reset"
  1059. >
  1060. 恢复所有默认
  1061. </button>
  1062. <button
  1063. type="button"
  1064. class="btn btn-primary"
  1065. data-bs-dismiss="modal"
  1066. data-action="save"
  1067. >
  1068. 保存设置
  1069. </button>
  1070. </div>
  1071. </div>
  1072. </div>
  1073. </div>`
  1074. );
  1075.  
  1076. body.querySelector(".modal-footer").addEventListener("click", e => {
  1077. const { action } = e.target.dataset;
  1078. if (!action) return;
  1079.  
  1080. e.preventDefault();
  1081. e.stopPropagation();
  1082.  
  1083. if (action === "save") {
  1084. const data = Object.fromEntries(new FormData(body.querySelector("form")).entries());
  1085. commands.forEach(key => GM_setValue(`${Domain}_${key}`, data[key] ?? ""));
  1086. }
  1087. if (action === "reset") {
  1088. GM_listValues().forEach(name => name.startsWith(Domain) && GM_deleteValue(name));
  1089. }
  1090. if (action === "restart") {
  1091. GM_listValues().forEach(name => GM_deleteValue(name));
  1092. }
  1093.  
  1094. location.reload();
  1095. });
  1096.  
  1097. const toggleIframe = () => {
  1098. DOC.body.parentNode.classList.toggle("x-scrollbar-hide");
  1099. iframe.classList.toggle("x-show");
  1100. };
  1101. const openModal = () => {
  1102. if (iframe.classList.contains("x-show")) return;
  1103. toggleIframe();
  1104. _DOC.querySelector("#openModal").click();
  1105. };
  1106. GM_registerMenuCommand("控制面板", openModal, "s");
  1107. _DOC.querySelector("#controlPanel").addEventListener("hidden.bs.modal", toggleIframe);
  1108. },
  1109. { once: true }
  1110. );
  1111. }
  1112.  
  1113. // styles
  1114. variables = `
  1115. :root {
  1116. --x-bgc: #121212;
  1117. --x-sub-bgc: #202020;
  1118. --x-ftc: #fffffff2;
  1119. --x-sub-ftc: #aaa;
  1120. --x-grey: #313131;
  1121. --x-blue: #0a84ff;
  1122. --x-orange: #ff9f0a;
  1123. --x-green: #30d158;
  1124. --x-red: #ff453a;
  1125. --x-line-h: 22px;
  1126. --x-thumb-w: 190px;
  1127. --x-cover-w: 295px;
  1128. --x-thumb-ratio: 334 / 473;
  1129. --x-cover-ratio: 135 / 91;
  1130. --x-avatar-ratio: 1;
  1131. --x-sprite-ratio: 4 / 3;
  1132. }
  1133. `;
  1134. style = `
  1135. ::-webkit-scrollbar {
  1136. width: 8px !important;
  1137. height: 8px !important;
  1138. }
  1139. ::-webkit-scrollbar-thumb {
  1140. background: #c1c1c1;
  1141. border-radius: 4px !important;
  1142. }
  1143. * {
  1144. text-decoration: none !important;
  1145. text-shadow: none !important;
  1146. outline: none !important;
  1147. }
  1148. `;
  1149. dmStyle = `
  1150. ::-webkit-scrollbar-thumb,
  1151. button {
  1152. background: var(--x-grey) !important;
  1153. }
  1154. * {
  1155. box-shadow: none !important;
  1156. }
  1157. *:not(span[class]) {
  1158. border-color: var(--x-grey) !important;
  1159. }
  1160. html,
  1161. body,
  1162. input,
  1163. textarea {
  1164. background: var(--x-bgc) !important;
  1165. }
  1166. body,
  1167. *::placeholder {
  1168. color: var(--x-sub-ftc) !important;
  1169. }
  1170. nav {
  1171. background: var(--x-sub-bgc) !important;
  1172. }
  1173. a,
  1174. button,
  1175. h1,
  1176. h2,
  1177. h3,
  1178. h4,
  1179. h5,
  1180. h6,
  1181. input,
  1182. p,
  1183. textarea {
  1184. color: var(--x-ftc) !important;
  1185. }
  1186. img {
  1187. filter: brightness(0.9) contrast(0.9) !important;
  1188. }
  1189. `;
  1190. customStyle = `
  1191. #x-status {
  1192. margin-bottom: 20px;
  1193. color: var(--x-sub-ftc);
  1194. font-size: 14px !important;
  1195. text-align: center;
  1196. }
  1197. .x-ml {
  1198. margin-left: 10px !important;
  1199. }
  1200. .x-mr {
  1201. margin-right: 10px !important;
  1202. }
  1203. .x-in {
  1204. opacity: 1 !important;
  1205. transition: opacity 0.25s linear;
  1206. }
  1207. .x-out {
  1208. opacity: 0 !important;
  1209. transition: opacity 0.25s linear;
  1210. }
  1211. .x-flex {
  1212. display: flex !important;
  1213. }
  1214. .x-cover {
  1215. width: var(--x-cover-w) !important;
  1216. }
  1217. .x-cover > *:first-child {
  1218. aspect-ratio: var(--x-cover-ratio) !important;
  1219. }
  1220. .x-ellipsis {
  1221. display: -webkit-box !important;
  1222. overflow: hidden;
  1223. white-space: unset !important;
  1224. text-overflow: ellipsis;
  1225. -webkit-line-clamp: 1;
  1226. -webkit-box-orient: vertical;
  1227. }
  1228. .x-line {
  1229. overflow: hidden;
  1230. white-space: nowrap;
  1231. text-overflow: ellipsis;
  1232. }
  1233. .x-title {
  1234. line-height: var(--x-line-h) !important;
  1235. }
  1236. .x-matched {
  1237. color: var(--x-blue) !important;
  1238. font-weight: bold;
  1239. }
  1240. .x-player {
  1241. position: relative;
  1242. display: block;
  1243. overflow: hidden;
  1244. }
  1245. .x-player::after {
  1246. position: absolute;
  1247. top: 0;
  1248. left: 0;
  1249. width: 100%;
  1250. height: 100%;
  1251. background-color: rgb(0 0 0 / 20%);
  1252. background-image: url(${GM_getResourceURL("play")});
  1253. background-repeat: no-repeat;
  1254. background-position: center;
  1255. background-size: 40px;
  1256. opacity: 0.85;
  1257. transition: all 0.25s ease-out;
  1258. content: "";
  1259. }
  1260. .x-player:hover::after {
  1261. background-color: rgb(0 0 0 / 0%);
  1262. }
  1263. .x-player img {
  1264. filter: none !important;
  1265. }
  1266. `;
  1267.  
  1268. // G_DARK
  1269. globalDark = (css = "", dark = "") => {
  1270. if (this.G_DARK) css += dark;
  1271. if (css) GM_addStyle(css.includes("var(--x-") ? `${this.variables}${css}` : css);
  1272. };
  1273. // G_SEARCH
  1274. globalSearch = (selectors, pathname) => {
  1275. if (!this.G_SEARCH) return;
  1276.  
  1277. DOC.addEventListener("keydown", async e => {
  1278. if (e.ctrlKey && e.keyCode === 191) {
  1279. const text = (await navigator.clipboard.readText()).trim();
  1280. if (!text) return;
  1281.  
  1282. openInTab(`${location.origin}${pathname.replace("%s", text)}`);
  1283. }
  1284. });
  1285.  
  1286. DOC.addEventListener("keyup", e => {
  1287. if (e.keyCode === 191 && !["INPUT", "TEXTAREA"].includes(DOC.activeElement.nodeName)) {
  1288. DOC.querySelector(selectors).focus();
  1289. }
  1290. });
  1291. };
  1292. // G_CLICK
  1293. globalClick = (selectors, node = DOC, callback) => {
  1294. node = node || DOC;
  1295.  
  1296. if (this.G_CLICK && this.D_MATCH && !this.listener.id && callback) {
  1297. this.listener.id = GM_addValueChangeListener("TEMPORARY_OBS", (name, old_value, new_value, remote) => {
  1298. if (!remote) return;
  1299. for (const url of unique(this.listener.list)) {
  1300. if (!new_value.includes(url)) continue;
  1301. Store.reduceTemporaryOb(url);
  1302. callback(url);
  1303. }
  1304. });
  1305. }
  1306.  
  1307. node.addEventListener("click", e => {
  1308. let { target } = e;
  1309.  
  1310. let url = "";
  1311. if (target.classList.contains("x-player")) {
  1312. url = `${this.pcUrl}${target.dataset.code}`;
  1313. } else if (this.G_CLICK) {
  1314. target = getTarget(e);
  1315. if (target) {
  1316. url = target.href;
  1317. if (this.D_MATCH && callback) this.listener.list.push(url);
  1318. }
  1319. }
  1320. if (!url) return;
  1321.  
  1322. e.preventDefault();
  1323. e.stopPropagation();
  1324. openInTab(url);
  1325. });
  1326.  
  1327. const getTarget = ({ target }) => {
  1328. const item = target.closest(selectors);
  1329. return !item?.href || !node.contains(item) ? false : item;
  1330. };
  1331.  
  1332. if (!this.G_CLICK) return;
  1333.  
  1334. let _event;
  1335.  
  1336. node.addEventListener("mousedown", e => {
  1337. if (e.button !== 2) return;
  1338.  
  1339. const target = getTarget(e);
  1340. if (!target) return;
  1341.  
  1342. e.preventDefault();
  1343. target.oncontextmenu = e => e.preventDefault();
  1344. _event = e;
  1345. });
  1346.  
  1347. node.addEventListener("mouseup", e => {
  1348. if (e.button !== 2) return;
  1349.  
  1350. const target = getTarget(e);
  1351. if (!_event || !target) return;
  1352.  
  1353. e.preventDefault();
  1354. const { clientX, clientY } = e;
  1355. const { clientX: _clientX, clientY: _clientY } = _event;
  1356. if (Math.abs(clientX - _clientX) + Math.abs(clientY - _clientY) > 5) return;
  1357.  
  1358. if (this.D_MATCH && callback) this.listener.list.push(target.href);
  1359. openInTab(target.href, false);
  1360. });
  1361. };
  1362.  
  1363. // L_MIT
  1364. listMovieImgType = (node, condition) => {
  1365. const img = node.querySelector("img");
  1366. if (!this.L_MIT || !img) return;
  1367.  
  1368. node.classList.add("x-cover");
  1369. img.loading = "lazy";
  1370. const { src = "" } = img;
  1371. img.src = condition.find(({ regex }) => regex.test(src))?.replace(src) ?? src;
  1372. };
  1373. // L_MTL, L_MTH
  1374. listMovieTitle = () => {
  1375. let num = parseInt(this.L_MTL ?? 0, 10);
  1376. if (this.L_MTH && num < 1) num = 1;
  1377.  
  1378. return `
  1379. .x-title {
  1380. -webkit-line-clamp: ${num <= 0 ? "unset" : num};
  1381. ${this.L_MTH ? `height: calc(var(--x-line-h) * ${num}) !important;` : ""}
  1382. }
  1383. `;
  1384. };
  1385. // L_SCROLL
  1386. listScroll = (container, itemSelector, path) => {
  1387. let msnry = {};
  1388. if (itemSelector) {
  1389. const items = container.querySelectorAll(itemSelector);
  1390. msnry = new Masonry(container, {
  1391. itemSelector,
  1392. columnWidth: items[items.length - 2] ?? items[items.length - 1],
  1393. fitWidth: true,
  1394. visibleStyle: { opacity: 1 },
  1395. hiddenStyle: { opacity: 0 },
  1396. });
  1397. container.classList.add("x-in");
  1398. }
  1399.  
  1400. if (!this.L_SCROLL) return;
  1401.  
  1402. let nextURL;
  1403. const updateNextURL = (node = DOC) => {
  1404. nextURL = node.querySelector(path)?.href;
  1405. };
  1406. updateNextURL();
  1407.  
  1408. const infScroll = new InfiniteScroll(container, {
  1409. path: () => nextURL,
  1410. checkLastPage: path,
  1411. outlayer: msnry,
  1412. history: false,
  1413. });
  1414.  
  1415. infScroll?.on("request", async (_, fetchPromise) => {
  1416. const { body } = await fetchPromise.then();
  1417. if (body) updateNextURL(body);
  1418. });
  1419.  
  1420. const status = DOC.create("div", { id: "x-status" });
  1421. container.insertAdjacentElement("afterend", status);
  1422. let textContent = "加载中...";
  1423. const noMore = "没有更多了";
  1424. try {
  1425. const path = infScroll.getPath() ?? "";
  1426. if (!path) textContent = noMore;
  1427. } catch (err) {
  1428. textContent = noMore;
  1429. }
  1430. status.textContent = textContent;
  1431.  
  1432. infScroll?.once("last", () => {
  1433. status.textContent = noMore;
  1434. });
  1435.  
  1436. return infScroll;
  1437. };
  1438.  
  1439. // M_IMG
  1440. movieImg = async ({ code }, start) => {
  1441. if (!this.M_IMG) return;
  1442.  
  1443. start && start();
  1444. let img = Store.getDetail(code)?.img;
  1445. if (!img?.length) {
  1446. img = await Apis.movieImg(code);
  1447. if (img?.length) Store.upDetail(code, { img });
  1448. }
  1449. return img;
  1450. };
  1451. // M_VIDEO
  1452. movieVideo = async ({ code, studio }, start) => {
  1453. if (!this.M_VIDEO) return;
  1454.  
  1455. start && start();
  1456. let video = Store.getDetail(code)?.video;
  1457. if (!video?.length) {
  1458. video = await Apis.movieVideo(code, studio);
  1459. if (video?.length) Store.upDetail(code, { video });
  1460. }
  1461. return video;
  1462. };
  1463. // M_PLAYER
  1464. moviePlayer = async ({ code }, start) => {
  1465. if (!this.M_PLAYER) return;
  1466.  
  1467. start && start();
  1468. let player = Store.getDetail(code)?.player;
  1469. if (!player?.length) {
  1470. player = await Apis.moviePlayer(code);
  1471. if (player?.length) Store.upDetail(code, { player });
  1472. }
  1473. return player;
  1474. };
  1475. // M_TITLE
  1476. movieTitle = async ({ code, title }, start) => {
  1477. if (!this.M_TITLE) return;
  1478.  
  1479. start && start();
  1480. let transTitle = Store.getDetail(code)?.transTitle;
  1481. if (!transTitle?.length) {
  1482. transTitle = await Apis.movieTitle(title);
  1483. if (transTitle?.length) Store.upDetail(code, { transTitle });
  1484. }
  1485. return transTitle;
  1486. };
  1487. // M_STAR
  1488. movieStar = async ({ code, star: hasStar }, start) => {
  1489. if (!this.M_STAR || hasStar) return;
  1490.  
  1491. start && start();
  1492. let star = Store.getDetail(code)?.star;
  1493. if (!star?.length) {
  1494. star = await Apis.movieStar(code);
  1495. if (star?.length) Store.upDetail(code, { star });
  1496. }
  1497. return star;
  1498. };
  1499. // M_JUMP
  1500. movieJump = start => {
  1501. if (!this.M_JUMP) return;
  1502. start && start();
  1503. };
  1504. // M_SUB
  1505. movieSub = (magnets, start) => {
  1506. if (!this.M_SUB) return magnets;
  1507.  
  1508. start && start();
  1509. const regex = /[A-Z]+.*-C/;
  1510. return magnets.map(item => {
  1511. item.zh = item.zh && !regex.test(item.name);
  1512. return item;
  1513. });
  1514. };
  1515. // M_SORT
  1516. movieSort = (magnets, start) => {
  1517. if (!this.M_SORT) return magnets;
  1518.  
  1519. start && start();
  1520. return magnets.length <= 1
  1521. ? magnets
  1522. : magnets.sort((pre, next) => {
  1523. if (pre.zh === next.zh) {
  1524. if (pre.bytes === next.bytes) return next.date - pre.date;
  1525. return next.bytes - pre.bytes;
  1526. } else {
  1527. return pre.zh > next.zh ? -1 : 1;
  1528. }
  1529. });
  1530. };
  1531. // M_MAGNET
  1532. movieMagnet = async ({ code }, start) => {
  1533. if (!this.M_MAGNET) return;
  1534.  
  1535. start && start();
  1536. let magnet = Store.getDetail(code)?.magnet;
  1537. if (!magnet?.length) {
  1538. magnet = unique(await Apis.movieMagnet(code), "link");
  1539. if (magnet?.length) Store.upDetail(code, { magnet });
  1540. }
  1541. return magnet;
  1542. };
  1543.  
  1544. // D_MATCH
  1545. driveMatch = async ({ code, res }, start) => {
  1546. if (!this.D_MATCH) return;
  1547.  
  1548. start && start();
  1549. code = code.toUpperCase();
  1550. const { prefix, regex } = codeParse(code);
  1551.  
  1552. if (res === "list") {
  1553. res = Store.getDetail(code)?.res;
  1554. if (!res?.length) {
  1555. const RESOURCE = GM_getValue("RESOURCE", []);
  1556. let item = RESOURCE.find(item => item.prefix === prefix);
  1557. if (!item) {
  1558. item = { prefix, res: await Apis.searchVideo(prefix) };
  1559. RESOURCE.push(item);
  1560. GM_setValue("RESOURCE", RESOURCE);
  1561. }
  1562. res = await this.driveMatch({ ...item, code });
  1563. }
  1564. } else {
  1565. let _res = res ?? (await Apis.searchVideo(prefix));
  1566. if (_res?.length) {
  1567. _res = _res.filter(({ n }) => regex.test(n));
  1568. }
  1569. if (!res) {
  1570. Store.upDetail(code, { res: _res });
  1571. if (this.G_CLICK) Store.addTemporaryOb(location.href);
  1572. }
  1573. res = _res;
  1574. }
  1575.  
  1576. return res;
  1577. };
  1578. // D_CID
  1579. driveCid = async () => {
  1580. let cid = this.D_CID;
  1581. if (/^\$\{.+\}$/.test(cid)) {
  1582. cid = cid.replace(/\$|\{|\}/g, "").trim();
  1583. const res = await Apis.getFile();
  1584. cid = res.find(({ n }) => n === cid)?.cid ?? "";
  1585. }
  1586. return cid;
  1587. };
  1588. // D_VERIFY
  1589. driveVerify = async ({ code, cid = "" }) => {
  1590. let verify = this.D_VERIFY <= 0;
  1591.  
  1592. for (let idx = 0; idx < this.D_VERIFY; idx++) {
  1593. await delay(1);
  1594.  
  1595. let res = await Apis.getVideo(cid);
  1596. res = await this.driveMatch({ code, res });
  1597.  
  1598. res = res.filter(({ t }) => t.startsWith(getDate()));
  1599. if (!res.length) continue;
  1600.  
  1601. const fids = (Store.getDetail(code)?.res ?? []).map(({ fid }) => fid);
  1602. res = res.filter(({ fid }) => !fids.includes(fid));
  1603. if (!res.length) continue;
  1604.  
  1605. verify = res;
  1606. break;
  1607. }
  1608.  
  1609. return verify;
  1610. };
  1611. // D_RENAME
  1612. driveRename = ({ cid, res, zh, code, title }) => {
  1613. let file_name = this.D_RENAME?.trim();
  1614. if (!file_name) return;
  1615.  
  1616. code = code.toUpperCase();
  1617. file_name = file_name
  1618. .replace(/\$\{字幕\}/g, zh ? "【中文字幕】" : "")
  1619. .replace(/\$\{番号\}/g, code)
  1620. .replace(/\$\{标题\}/g, title);
  1621.  
  1622. if (!codeParse(code).regex.test(file_name)) file_name = `${code} - ${file_name}`;
  1623.  
  1624. const numRegex = /\$\{序号\}/g;
  1625. res = res
  1626. .filter(item => item.ico)
  1627. .map((item, index) => {
  1628. item.file_name = `${file_name.replace(numRegex, index + 1)}.${item.ico}`;
  1629. return item;
  1630. });
  1631.  
  1632. unique(res.map(item => item.cid).filter(item => item !== cid)).forEach(fid =>
  1633. res.push({ fid, file_name: file_name.replace(numRegex, "") })
  1634. );
  1635.  
  1636. return Apis.driveRename(res);
  1637. };
  1638. // OFFLINE
  1639. driveOffline = async (e, { magnets, code, title }) => {
  1640. const { target } = e;
  1641. const { magnet: type } = target.dataset;
  1642. if (!type) return;
  1643.  
  1644. e.preventDefault();
  1645. e.stopPropagation();
  1646.  
  1647. const { classList } = target;
  1648. if (classList.contains("active")) return;
  1649.  
  1650. classList.add("active");
  1651. const originText = target.textContent;
  1652. target.textContent = "请求中...";
  1653. target.setAttribute("disabled", "disabled");
  1654.  
  1655. const wp_path_id = await this.driveCid();
  1656.  
  1657. if (type === "all") {
  1658. const warnMsg = { title: "一键离线任务失败", image: "warn" };
  1659. const successMsg = { title: "一键离线任务成功", image: "success", timeout: 3000 };
  1660.  
  1661. const magnetLen = magnets.length;
  1662.  
  1663. for (let index = 0; index < magnetLen; index++) {
  1664. const sign = await Apis.getSign();
  1665. if (!sign) break;
  1666.  
  1667. const isLast = index + 1 === magnetLen;
  1668. const { link: url, zh } = magnets[index];
  1669.  
  1670. let res = await Apis.addTaskUrl({ url, wp_path_id, ...sign });
  1671. if (!res?.state) {
  1672. if (res.errcode === 911) {
  1673. notify({ title: "一键离线任务中断", text: res.error_msg, image: "warn", highlight: false });
  1674. break;
  1675. }
  1676. if (!isLast) continue;
  1677. notify(warnMsg);
  1678. break;
  1679. }
  1680.  
  1681. const cid = wp_path_id;
  1682.  
  1683. res = await this.driveVerify({ code, cid });
  1684. if (!res) {
  1685. if (!isLast) continue;
  1686. notify(warnMsg);
  1687. break;
  1688. }
  1689.  
  1690. if (res?.length) {
  1691. successMsg.text = "点击跳转目录";
  1692. successMsg.clickUrl = `https://115.com/?cid=${res[0].cid}&offset=0&mode=wangpan`;
  1693. await this.driveRename({ cid, res, zh, code, title });
  1694. }
  1695.  
  1696. notify(successMsg);
  1697. break;
  1698. }
  1699. } else if (type) {
  1700. const url = type;
  1701. const res = await Apis.addTaskUrl({ url, wp_path_id });
  1702. if (res) {
  1703. notify({
  1704. title: `离线任务添加${res.state ? "成功" : "失败"}`,
  1705. text: res.error_msg ?? "",
  1706. image: res.state ? "info" : "warn",
  1707. highlight: false,
  1708. });
  1709. }
  1710. }
  1711.  
  1712. classList.remove("active");
  1713. target.textContent = originText;
  1714. target.removeAttribute("disabled");
  1715. };
  1716. }
  1717.  
  1718. // javbus
  1719. class JavBus extends Common {
  1720. constructor() {
  1721. super();
  1722. return super.init();
  1723. }
  1724.  
  1725. routes = {
  1726. list: /^\/((uncensored|uncensored\/)?(page\/\d+)?$)|((uncensored\/)?((search|searchstar|actresses|genre|star|studio|label|series|director|member)+\/)|actresses(\/\d+)?)+/i,
  1727. genre: /^\/(uncensored\/)?genre$/i,
  1728. forum: /^\/forum\//i,
  1729. movie: /^\/[\w]+(-|_)?[\d]*.*$/i,
  1730. };
  1731.  
  1732. // styles
  1733. _style = `
  1734. body {
  1735. overflow-y: overlay;
  1736. }
  1737. .ad-box {
  1738. display: none !important;
  1739. }
  1740. footer {
  1741. display: none !important;
  1742. }
  1743. `;
  1744. _dmStyle = `
  1745. .nav > li > a:hover,
  1746. .nav > li > a:focus,
  1747. .dropdown-menu > li > a:hover,
  1748. .dropdown-menu > li > a:focus {
  1749. background: var(--x-grey) !important;
  1750. }
  1751. .nav > li.active > a,
  1752. .nav > .open > a,
  1753. .nav > .open > a:hover,
  1754. .nav > .open > a:focus,
  1755. .dropdown-menu {
  1756. background: var(--x-bgc) !important;
  1757. }
  1758. .modal-content, .alert {
  1759. background: var(--x-sub-bgc) !important;
  1760. }
  1761. .btn-primary {
  1762. background: var(--x-blue) !important;
  1763. border-color: var(--x-blue) !important;
  1764. }
  1765. .btn-success {
  1766. background: var(--x-green) !important;
  1767. border-color: var(--x-green) !important;
  1768. }
  1769. .btn-warning {
  1770. background: var(--x-orange) !important;
  1771. border-color: var(--x-orange) !important;
  1772. }
  1773. .btn-danger {
  1774. background: var(--x-red) !important;
  1775. border-color: var(--x-red) !important;
  1776. }
  1777. .btn.disabled, .btn[disabled], fieldset[disabled] .btn {
  1778. opacity: .8 !important;
  1779. }
  1780. `;
  1781. boxStyle = `
  1782. .movie-box,
  1783. .avatar-box,
  1784. .sample-box {
  1785. width: var(--x-thumb-w) !important;
  1786. margin: 10px !important;
  1787. border: none !important;
  1788. }
  1789. .movie-box .photo-frame,
  1790. .avatar-box .photo-frame,
  1791. .sample-box .photo-frame {
  1792. height: auto !important;
  1793. margin: 10px !important;
  1794. border: none !important;
  1795. }
  1796. .movie-box .photo-frame {
  1797. aspect-ratio: var(--x-thumb-ratio);
  1798. }
  1799. .avatar-box .photo-frame {
  1800. aspect-ratio: var(--x-avatar-ratio);
  1801. }
  1802. .sample-box .photo-frame {
  1803. aspect-ratio: var(--x-sprite-ratio);
  1804. }
  1805. .movie-box img,
  1806. .avatar-box img,
  1807. .sample-box img {
  1808. width: 100% !important;
  1809. min-width: unset !important;
  1810. max-width: none !important;
  1811. height: 100% !important;
  1812. min-height: unset !important;
  1813. max-height: none !important;
  1814. margin: 0 !important;
  1815. object-fit: cover !important;
  1816. }
  1817. .movie-box > *,
  1818. .avatar-box > *,
  1819. .sample-box > * {
  1820. background: unset !important;
  1821. }
  1822. .movie-box > *:nth-child(2),
  1823. .avatar-box > *:nth-child(2),
  1824. .sample-box > *:nth-child(2) {
  1825. height: auto !important;
  1826. padding: 0 10px 10px !important;
  1827. line-height: var(--x-line-h) !important;
  1828. border: none !important;
  1829. }
  1830. `;
  1831. dmBoxStyle = `
  1832. .movie-box,
  1833. .avatar-box,
  1834. .sample-box {
  1835. background: var(--x-sub-bgc) !important;
  1836. }
  1837. .movie-box > *:nth-child(2),
  1838. .avatar-box > *:nth-child(2),
  1839. .sample-box > *:nth-child(2) {
  1840. color: unset;
  1841. }
  1842. .movie-box date {
  1843. color: var(--x-sub-ftc) !important;
  1844. }
  1845. `;
  1846.  
  1847. // methods
  1848. _globalSearch = () => {
  1849. this.globalSearch("#search-input", "/search/%s");
  1850. };
  1851. _globalClick = callback => {
  1852. this.globalClick([".movie-box", ".avatar-box"], "", callback);
  1853. };
  1854. modifyMovieBox = (node = DOC) => {
  1855. const items = node.querySelectorAll(".movie-box");
  1856. for (const item of items) {
  1857. const info = item.querySelector(".photo-info span");
  1858. info.innerHTML = info.innerHTML.replace(/<\/?span.*>|<br>/g, "");
  1859. const title = info.firstChild;
  1860. if (!title) continue;
  1861. const titleText = title.textContent.trim();
  1862. const _title = DOC.create("div", { title: titleText, class: "x-ellipsis x-title" }, titleText);
  1863. info.replaceChild(_title, title);
  1864. }
  1865. };
  1866.  
  1867. // modules
  1868. list = {
  1869. docStart() {
  1870. const style = `
  1871. #waterfall {
  1872. display: none;
  1873. opacity: 0;
  1874. }
  1875. #waterfall .item {
  1876. float: unset !important;
  1877. }
  1878. #waterfall img {
  1879. opacity: 0;
  1880. }
  1881. .search-header {
  1882. padding: 0 !important;
  1883. background: none !important;
  1884. box-shadow: none !important;
  1885. }
  1886. .search-header .nav-tabs {
  1887. display: none !important;
  1888. }
  1889. .alert-common {
  1890. margin: 20px 20px 0 !important;
  1891. }
  1892. .alert-page {
  1893. margin: 20px !important;
  1894. }
  1895. .text-center.hidden-xs {
  1896. display: none;
  1897. line-height: 0;
  1898. }
  1899. ul.pagination {
  1900. margin-bottom: 40px;
  1901. }
  1902. .movie-box .x-title + div {
  1903. height: var(--x-line-h) !important;
  1904. margin: 4px 0;
  1905. }
  1906. .avatar-box .pb10 {
  1907. padding: 0 !important;
  1908. }
  1909. .avatar-box .pb10:not(:last-child) {
  1910. margin-bottom: 4px !important;
  1911. }
  1912. .avatar-box p {
  1913. margin: 0 0 6px !important;
  1914. }
  1915. .mleft {
  1916. display: flex !important;
  1917. align-items: center;
  1918. }
  1919. .mleft .btn-xs {
  1920. margin: 0 6px 0 0 !important;
  1921. }
  1922. `;
  1923. const dmStyle = `
  1924. .pagination > li > a {
  1925. color: var(--x-ftc) !important;
  1926. background-color: var(--x-sub-bgc) !important;
  1927. }
  1928. .nav-pills > li.active > a {
  1929. background-color: var(--x-blue) !important;
  1930. }
  1931. .pagination > li:not(.active) > a:hover {
  1932. background-color: var(--x-grey) !important;
  1933. }
  1934. `;
  1935. this.globalDark(
  1936. `${this.style}${this._style}${this.boxStyle}${this.customStyle}${style}${this.listMovieTitle()}`,
  1937. `${this.dmStyle}${this._dmStyle}${this.dmBoxStyle}${dmStyle}`
  1938. );
  1939. },
  1940. contentLoaded() {
  1941. const nav = DOC.querySelector(".search-header .nav");
  1942. if (nav) nav.classList.replace("nav-tabs", "nav-pills");
  1943.  
  1944. this._globalSearch();
  1945. this._globalClick(url => {
  1946. const node = DOC.querySelector(`a.movie-box[href="${url}"]`);
  1947. if (node) this.updateMatchStatus(node);
  1948. });
  1949.  
  1950. this.modifyLayout();
  1951. },
  1952. modifyLayout() {
  1953. const waterfall = DOC.querySelector("#waterfall");
  1954. if (!waterfall) return;
  1955.  
  1956. const isStarDetail = /^\/(uncensored\/)?star\/\w+/i.test(location.pathname);
  1957.  
  1958. const _waterfall = waterfall.cloneNode(true);
  1959. _waterfall.removeAttribute("style");
  1960. _waterfall.setAttribute("class", "x-show");
  1961. const items = this.modifyItem(_waterfall);
  1962.  
  1963. const itemsLen = items?.length;
  1964. if (itemsLen) {
  1965. _waterfall.innerHTML = "";
  1966. for (let index = 0; index < itemsLen; index++) {
  1967. if (isStarDetail && !index) continue;
  1968. _waterfall.appendChild(items[index]);
  1969. }
  1970. }
  1971. waterfall.parentElement.replaceChild(_waterfall, waterfall);
  1972.  
  1973. const infScroll = this.listScroll(_waterfall, ".item", "#next");
  1974. if (!infScroll) return DOC.querySelector(".text-center.hidden-xs")?.classList.add("x-show");
  1975.  
  1976. infScroll?.on("request", async (_, fetchPromise) => {
  1977. const { body } = await fetchPromise.then();
  1978. if (!body) return;
  1979.  
  1980. let items = this.modifyItem(body);
  1981. if (isStarDetail) [_, ...items] = items;
  1982. infScroll.appendItems(items);
  1983. infScroll.options.outlayer.appended(items);
  1984. });
  1985. },
  1986. modifyItem(container) {
  1987. const items = container.querySelectorAll(".item");
  1988. for (const item of items) {
  1989. item.removeAttribute("style");
  1990. item.setAttribute("class", "item");
  1991. this._listMovieImgType(item);
  1992. this.modifyAvatarBox(item);
  1993. this.modifyMovieBox(item);
  1994. }
  1995. fadeInImg(items);
  1996. this._driveMatch(container);
  1997. return items;
  1998. },
  1999. _listMovieImgType(node) {
  2000. const item = node.querySelector(".movie-box");
  2001. if (!item) return;
  2002.  
  2003. const condition = [
  2004. {
  2005. regex: /\/thumb(s)?\//gi,
  2006. replace: val => val.replace(/\/thumb(s)?\//gi, "/cover/").replace(/\.jpg/gi, "_b.jpg"),
  2007. },
  2008. {
  2009. regex: /pics\.dmm\.co\.jp/gi,
  2010. replace: val => val.replace("ps.jpg", "pl.jpg"),
  2011. },
  2012. ];
  2013. this.listMovieImgType(item, condition);
  2014. },
  2015. modifyAvatarBox(node = DOC) {
  2016. const items = node.querySelectorAll(".avatar-box");
  2017. for (const item of items) {
  2018. const span = item.querySelector("span");
  2019. if (span.classList.contains("mleft")) {
  2020. const title = span.firstChild;
  2021. const titleText = title.textContent.trim();
  2022. const _title = DOC.create("div", { title: titleText, class: "x-line" }, titleText);
  2023. title.parentElement.replaceChild(_title, title);
  2024. span.insertAdjacentElement("afterbegin", span.querySelector("button"));
  2025. continue;
  2026. }
  2027. span.classList.add("x-line");
  2028. }
  2029. },
  2030. async _driveMatch(node = DOC) {
  2031. const items = node.querySelectorAll(".movie-box");
  2032. for (const item of items) await this.updateMatchStatus(item);
  2033. },
  2034. async updateMatchStatus(node) {
  2035. const code = node.querySelector("date")?.textContent?.trim();
  2036. if (!code) return;
  2037.  
  2038. const res = await this.driveMatch({ code, res: "list" });
  2039. if (!res?.length) return;
  2040.  
  2041. const frame = node.querySelector(".photo-frame");
  2042. frame.classList.add("x-player");
  2043. frame.setAttribute("title", "点击播放");
  2044. frame.setAttribute("data-code", res[0].pc);
  2045. node.querySelector(".x-title").classList.add("x-matched");
  2046. },
  2047. };
  2048. genre = {
  2049. docStart() {
  2050. const style = `
  2051. .alert-common {
  2052. margin: 20px 0 0 !important;
  2053. }
  2054. .container-fluid {
  2055. padding: 0 20px !important;
  2056. }
  2057. h4 {
  2058. margin: 20px 0 10px 0 !important;
  2059. }
  2060. .genre-box {
  2061. margin: 10px 0 20px 0 !important;
  2062. padding: 20px !important;
  2063. }
  2064. .genre-box a {
  2065. text-align: left !important;
  2066. cursor: pointer !important;
  2067. user-select: none !important;
  2068. }
  2069. .genre-box input {
  2070. margin: 0 !important;
  2071. vertical-align: middle !important;
  2072. }
  2073. .x-last-box {
  2074. margin-bottom: 70px !important;
  2075. }
  2076. button.btn.btn-danger.btn-block.btn-genre {
  2077. position: fixed !important;
  2078. bottom: 0 !important;
  2079. left: 0 !important;
  2080. margin: 0 !important;
  2081. border: none !important;
  2082. border-radius: 0 !important;
  2083. }
  2084. `;
  2085. const dmStyle = `
  2086. .genre-box {
  2087. background: var(--x-sub-bgc) !important;
  2088. }
  2089. `;
  2090. this.globalDark(`${this.style}${this._style}${style}`, `${this.dmStyle}${this._dmStyle}${dmStyle}`);
  2091. },
  2092. contentLoaded() {
  2093. this._globalSearch();
  2094. if (!DOC.querySelector("button.btn.btn-danger.btn-block.btn-genre")) return;
  2095.  
  2096. const box = DOC.querySelectorAll(".genre-box");
  2097. box[box.length - 1].classList.add("x-last-box");
  2098. DOC.querySelector(".container-fluid.pt10").addEventListener("click", ({ target }) => {
  2099. if (target.nodeName !== "A" || !target.classList.contains("text-center")) return;
  2100. const checkbox = target.querySelector("input");
  2101. checkbox.checked = !checkbox.checked;
  2102. });
  2103. },
  2104. };
  2105. forum = {
  2106. docStart() {
  2107. const style = `
  2108. .bcpic,
  2109. .banner728,
  2110. .banner300,
  2111. .jav-footer {
  2112. display: none !important;
  2113. }
  2114. #toptb {
  2115. position: fixed !important;
  2116. top: 0 !important;
  2117. right: 0 !important;
  2118. left: 0 !important;
  2119. z-index: 999 !important;
  2120. border-color: #e7e7e7;
  2121. box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
  2122. }
  2123. #search-input {
  2124. border-right: none !important;
  2125. }
  2126. .jav-button {
  2127. margin-top: -3px !important;
  2128. margin-left: -4px !important;
  2129. }
  2130. #wp {
  2131. margin-top: 55px !important;
  2132. }
  2133. .biaoqicn_show a {
  2134. width: 20px !important;
  2135. height: 20px !important;
  2136. line-height: 20px !important;
  2137. opacity: .7;
  2138. }
  2139. #online .bm_h {
  2140. border-bottom: none !important;
  2141. }
  2142. #online .bm_h .o img {
  2143. margin-top: 48%;
  2144. }
  2145. #moquu_top {
  2146. right: 20px;
  2147. bottom: 20px;
  2148. }
  2149. `;
  2150. this.globalDark(`${this.style}${this._style}${style}`);
  2151. },
  2152. contentLoaded() {
  2153. this._globalSearch();
  2154. },
  2155. };
  2156. movie = {
  2157. params: {},
  2158. magnets: null,
  2159.  
  2160. docStart() {
  2161. const style = `
  2162. #mag-submit-show,
  2163. #mag-submit,
  2164. #magnet-table,
  2165. h4[style="position:relative"],
  2166. h4[style="position:relative"] + .row,
  2167. .info span.glyphicon-info-sign {
  2168. display: none !important;
  2169. }
  2170. html {
  2171. padding-right: 0 !important;
  2172. }
  2173. @media (min-width: 1270px) {
  2174. .container { width: 1270px; }
  2175. }
  2176. .container {
  2177. margin-bottom: 40px;
  2178. }
  2179. .row.movie {
  2180. padding: 0 !important;
  2181. }
  2182. #magneturlpost + .movie {
  2183. margin-top: 20px !important;
  2184. padding: 10px !important;
  2185. }
  2186. .screencap, .info {
  2187. padding: 10px !important;
  2188. border: none !important;
  2189. }
  2190. .bigImage {
  2191. position: relative;
  2192. display: block;
  2193. overflow: hidden;
  2194. opacity: 0;
  2195. aspect-ratio: var(--x-cover-ratio);
  2196. }
  2197. .info p {
  2198. line-height: var(--x-line-h) !important;
  2199. }
  2200. .star-box img {
  2201. width: 100% !important;
  2202. height: auto !important;
  2203. margin: 0 !important;
  2204. }
  2205. .star-box .star-name {
  2206. padding: 6px 0 !important;
  2207. background: unset !important;
  2208. border: none !important;
  2209. }
  2210. #avatar-waterfall,
  2211. #sample-waterfall,
  2212. #related-waterfall {
  2213. margin: -10px !important;
  2214. word-spacing: -20px;
  2215. }
  2216. .avatar-box,
  2217. .sample-box,
  2218. .movie-box {
  2219. vertical-align: top !important;
  2220. word-spacing: 0 !important;
  2221. }
  2222. .movie-box > *:nth-child(2) {
  2223. min-height: 32px !important;
  2224. text-align: left !important;
  2225. }
  2226. .x-grass-img {
  2227. object-fit: cover;
  2228. }
  2229. .x-grass-mask,
  2230. .x-contain {
  2231. position: absolute;
  2232. top: 0;
  2233. left: 0;
  2234. width: 100% !important;
  2235. height: 100% !important;
  2236. }
  2237. .x-grass-mask {
  2238. backdrop-filter: blur(50px);
  2239. }
  2240. .x-contain {
  2241. z-index: -1;
  2242. object-fit: contain;
  2243. border: none;
  2244. opacity: 0;
  2245. }
  2246. video.x-contain {
  2247. background-color: #000;
  2248. }
  2249. .x-contain.x-in {
  2250. z-index: 9 !important;
  2251. }
  2252. .mfp-img {
  2253. max-height: unset !important;
  2254. }
  2255. .btn-group button {
  2256. border-width: .5px !important;
  2257. }
  2258. .x-res * {
  2259. color: #CC0000 !important;
  2260. }
  2261. .x-table {
  2262. margin: 0 !important;
  2263. }
  2264. .x-caption {
  2265. display: flex;
  2266. align-items: center;
  2267. }
  2268. .x-caption .label {
  2269. position: unset !important;
  2270. }
  2271. .x-table tr {
  2272. display: table;
  2273. width: 100%;
  2274. table-layout: fixed;
  2275. }
  2276. .x-table tr > * {
  2277. vertical-align: middle !important;
  2278. border-left: none !important;
  2279. }
  2280. .x-table tr > *:first-child {
  2281. width: 50px;
  2282. }
  2283. .x-table tr > *:nth-child(2) {
  2284. width: 33.3%;
  2285. }
  2286. .x-table tr > *:last-child,
  2287. .x-table tfoot tr > th:not(:nth-child(3)) {
  2288. border-right: none !important;
  2289. }
  2290. .x-table tbody {
  2291. display: block;
  2292. max-height: calc(37px * 10 - 1px);
  2293. overflow: overlay;
  2294. table-layout: fixed;
  2295. }
  2296. .x-table tbody tr > * {
  2297. border-top: none !important;
  2298. }
  2299. .x-table tbody tr:last-child > *,
  2300. .x-table tfoot tr > * {
  2301. border-bottom: none !important;
  2302. }
  2303. `;
  2304. const dmStyle = `
  2305. .movie,
  2306. .btn-group button[disabled],
  2307. .star-box-up li,
  2308. .table-striped > tbody > tr:nth-of-type(odd) {
  2309. background: var(--x-sub-bgc) !important;
  2310. }
  2311. .btn-group button.active,
  2312. .x-offline.active {
  2313. background: var(--x-bgc) !important;
  2314. }
  2315. tbody tr:hover,
  2316. .table-striped > tbody > tr:nth-of-type(odd):hover {
  2317. background: var(--x-grey) !important;
  2318. }
  2319. `;
  2320. this.globalDark(
  2321. `${this.style}${this._style}${this.boxStyle}${this.customStyle}${style}`,
  2322. `${this.dmStyle}${this._dmStyle}${this.dmBoxStyle}${dmStyle}`
  2323. );
  2324. },
  2325. contentLoaded() {
  2326. this._globalSearch();
  2327. this._globalClick();
  2328.  
  2329. this.params = this.getParams();
  2330.  
  2331. addCopyTarget("h3", { title: "复制标题" });
  2332.  
  2333. this.initSwitch();
  2334. this.updateSwitch({ key: "img", title: "大图" });
  2335. this.updateSwitch({ key: "video", title: "预览" });
  2336. this.updateSwitch({ key: "player", title: "视频", type: "video" });
  2337.  
  2338. this._movieTitle();
  2339. addCopyTarget("span[style='color:#CC0000;']", { title: "复制番号" });
  2340. this._movieJump();
  2341. this._movieStar();
  2342.  
  2343. this._driveMatch();
  2344. DOC.querySelector(".x-offline")?.addEventListener("click", e => this._driveOffline(e));
  2345.  
  2346. const tableObs = new MutationObserver((_, obs) => {
  2347. obs.disconnect();
  2348. this.refactorTable();
  2349. });
  2350. tableObs.observe(DOC.querySelector("#movie-loading"), { attributes: true, attributeFilter: ["style"] });
  2351.  
  2352. this.modifyMovieBox();
  2353. },
  2354. getParams() {
  2355. const info = DOC.querySelector(".info");
  2356. const { textContent } = info;
  2357. return {
  2358. title: DOC.querySelector("h3").textContent,
  2359. code: info.querySelector("span[style='color:#CC0000;']").textContent,
  2360. date: info.querySelector("p:nth-child(2)").childNodes[1].textContent.trim(),
  2361. studio: textContent.match(/(?<=製作商: ).+/g)?.pop(0),
  2362. star: !/暫無出演者資訊/g.test(textContent),
  2363. };
  2364. },
  2365. _movieJump() {
  2366. const { code } = this.params;
  2367. if (!code) return;
  2368.  
  2369. const start = () => {
  2370. DOC.querySelector("span[style='color:#CC0000;']")?.insertAdjacentHTML(
  2371. "beforeend",
  2372. `<a class="x-ml" href="https://javdb.com/search?q=${code}#jump" title="跳转 JavDB">跳转</a>`
  2373. );
  2374. };
  2375. this.movieJump(start);
  2376. },
  2377. initSwitch() {
  2378. const bigImage = DOC.querySelector(".bigImage");
  2379.  
  2380. const img = bigImage.querySelector("img");
  2381. img.classList.add("x-grass-img");
  2382.  
  2383. bigImage.insertAdjacentHTML(
  2384. "beforeend",
  2385. `<div class="x-grass-mask"></div><img src="${img.src}" id="x-switch-cover" class="x-contain x-in">`
  2386. );
  2387. bigImage.classList.add("x-in");
  2388.  
  2389. const info = DOC.querySelector(".info");
  2390. info.insertAdjacentHTML(
  2391. "afterbegin",
  2392. `<div class="btn-group btn-group-justified mb10" hidden id="x-switch" role="group">
  2393. <div class="btn-group btn-group-sm" role="group" title="点击放大">
  2394. <button type="button" class="btn btn-default active" for="x-switch-cover">封面</button>
  2395. </div>
  2396. </div>`
  2397. );
  2398.  
  2399. const switcher = info.querySelector("#x-switch");
  2400. switcher.addEventListener("click", ({ target }) => {
  2401. const id = target.getAttribute("for");
  2402. if (!id) return;
  2403.  
  2404. const { classList } = target;
  2405. const curActive = bigImage.querySelector(".x-contain.x-in");
  2406.  
  2407. if (classList.contains("active")) {
  2408. curActive.nodeName === "VIDEO" ? (curActive.muted = !curActive.muted) : bigImage.click();
  2409. return;
  2410. }
  2411.  
  2412. curActive.classList.toggle("x-in");
  2413. curActive?.pause && curActive.pause();
  2414. const active = bigImage.querySelector(`#${id}`);
  2415. active.classList.toggle("x-in");
  2416. if (active.nodeName === "IMG") {
  2417. const { src } = active;
  2418. bigImage.href = src;
  2419. bigImage.querySelector(".x-grass-img").src = src;
  2420. }
  2421. active?.play && active.play();
  2422. active?.focus && active.focus();
  2423.  
  2424. const curTarget = switcher.querySelector("button.active");
  2425. curTarget.classList.toggle("active");
  2426. curTarget.setAttribute("title", "点击切换");
  2427. classList.toggle("active");
  2428. target.removeAttribute("title");
  2429. });
  2430. },
  2431. async updateSwitch({ key, title, type }) {
  2432. if (!type) type = key;
  2433. const id = `x-switch-${key}`;
  2434.  
  2435. const switcher = DOC.querySelector("#x-switch");
  2436. const start = () => {
  2437. if (!switcher.classList.contains("x-show")) switcher.classList.add("x-show");
  2438. switcher.insertAdjacentHTML(
  2439. "beforeend",
  2440. `<div class="btn-group btn-group-sm" role="group">
  2441. <button type="button" class="btn btn-default" for="${id}" disabled>查询中...</button>
  2442. </div>`
  2443. );
  2444. };
  2445.  
  2446. const src = await this[`movie${key[0].toUpperCase()}${key.slice(1)}`](this.params, start);
  2447. const node = switcher.querySelector(`button[for="${id}"]`);
  2448. if (!node) return;
  2449.  
  2450. node.textContent = `${src?.length ? "查看" : "暂无"}${title}`;
  2451. if (!src?.length) return;
  2452.  
  2453. node.parentNode.setAttribute("title", "点击放大或切换静音");
  2454. node.removeAttribute("disabled");
  2455. node.setAttribute("title", "点击切换");
  2456.  
  2457. const item = DOC.create(type, { id, class: "x-contain" });
  2458. if (typeof src === "string") item.src = src;
  2459.  
  2460. if (type === "video") {
  2461. if (Object.prototype.toString.call(src) === "[object Array]") {
  2462. src.forEach(params => {
  2463. const source = DOC.create("source", params);
  2464. item.appendChild(source);
  2465. });
  2466. }
  2467.  
  2468. item.controls = true;
  2469. item.currentTime = 3;
  2470. item.muted = true;
  2471. item.preload = "metadata";
  2472. item.addEventListener("click", e => {
  2473. e.preventDefault();
  2474. e.stopPropagation();
  2475. const { target: video } = e;
  2476. video.paused ? video.play() : video.pause();
  2477. });
  2478. }
  2479.  
  2480. DOC.querySelector(".bigImage").insertAdjacentElement("beforeend", item);
  2481. },
  2482. async _movieTitle() {
  2483. const start = () => {
  2484. DOC.querySelector("#x-switch").insertAdjacentHTML(
  2485. "afterend",
  2486. `<p><span class="header">机翻标题: </span><span class="x-transTitle">查询中...</span></p>`
  2487. );
  2488. };
  2489.  
  2490. const transTitle = await this.movieTitle(this.params, start);
  2491. const transTitleNode = DOC.querySelector(".x-transTitle");
  2492. if (transTitleNode) transTitleNode.textContent = transTitle ?? "查询失败";
  2493. },
  2494. async _movieStar() {
  2495. const start = () => {
  2496. const starShow = DOC.querySelector("p.star-show");
  2497. starShow.nextElementSibling.nextSibling.remove();
  2498. starShow.insertAdjacentHTML("afterend", `<p class="x-star">查询中...</p>`);
  2499. };
  2500.  
  2501. const star = await this.movieStar(this.params, start);
  2502. const starNode = DOC.querySelector(".x-star");
  2503. if (!starNode) return;
  2504.  
  2505. starNode.innerHTML = !star?.length
  2506. ? "暂无演员数据"
  2507. : star.reduce(
  2508. (acc, cur) =>
  2509. `${acc}<span class="genre"><label><a href="/search/${cur}">${cur}</a></label></span>`,
  2510. ""
  2511. );
  2512. },
  2513. async _driveMatch() {
  2514. const start = () => {
  2515. if (DOC.querySelector(".x-res")) return;
  2516.  
  2517. GM_addStyle(`tbody a[data-magnet] { display: inline !important; }`);
  2518. DOC.querySelector(".info").insertAdjacentHTML(
  2519. "beforeend",
  2520. `<p class="header">网盘资源:</p><p class="x-res">查询中...</p><button type="button" class="btn btn-default btn-sm btn-block x-offline" data-magnet="all">一键离线</button>`
  2521. );
  2522. };
  2523.  
  2524. const res = await this.driveMatch(this.params, start);
  2525. const resNode = DOC.querySelector(".x-res");
  2526. if (!resNode) return;
  2527.  
  2528. resNode.innerHTML = !res?.length
  2529. ? "暂无网盘资源"
  2530. : res.reduce(
  2531. (acc, { pc, t, n }) =>
  2532. `${acc}<div class="x-line"><a href="${this.pcUrl}${pc}" target="_blank" title="${t} / ${n}">${n}</a></div>`,
  2533. ""
  2534. );
  2535. },
  2536. refactorTable() {
  2537. const table = DOC.querySelector("#magnet-table");
  2538. table.parentElement.innerHTML = `
  2539. <table class="table table-striped table-hover table-bordered x-table">
  2540. <caption><div class="x-caption">重构的表格</div></caption>
  2541. <thead>
  2542. <tr>
  2543. <th scope="col">#</th>
  2544. <th scope="col">磁力名称</th>
  2545. <th scope="col">档案大小</th>
  2546. <th scope="col" class="text-center">分享日期</th>
  2547. <th scope="col" class="text-center">来源</th>
  2548. <th scope="col" class="text-center">字幕</th>
  2549. <th scope="col">操作</th>
  2550. </tr>
  2551. </thead>
  2552. <tbody>
  2553. <tr><th scope="row" colspan="7" class="text-center text-muted">暂无数据</th></tr>
  2554. </tbody>
  2555. <tfoot>
  2556. <tr>
  2557. <th scope="row"></th>
  2558. <th></th>
  2559. <th colspan="4" class="text-right">总数</th>
  2560. <td>0</td>
  2561. </tr>
  2562. </tfoot>
  2563. </table>
  2564. `;
  2565.  
  2566. DOC.querySelector(".x-table tbody").addEventListener("click", e => {
  2567. !handleCopyTxt(e, "复制成功") && this._driveOffline(e);
  2568. });
  2569.  
  2570. const magnets = [];
  2571. for (const tr of table.querySelectorAll("tr")) {
  2572. const [link, size, date] = tr.querySelectorAll("td");
  2573. const _link = link?.querySelector("a");
  2574. const _size = size?.textContent.trim();
  2575. if (!_link || !_size || !date) continue;
  2576.  
  2577. magnets.push({
  2578. name: _link.textContent.trim(),
  2579. link: _link.href.split("&")[0],
  2580. zh: !!link.querySelector("a.btn.btn-mini-new.btn-warning.disabled"),
  2581. size: _size,
  2582. bytes: transToBytes(_size),
  2583. date: date.textContent.trim(),
  2584. });
  2585. }
  2586.  
  2587. this.refactorTd(magnets);
  2588. this._movieMagnet();
  2589. },
  2590. async _movieMagnet() {
  2591. const start = () => {
  2592. DOC.querySelector(".x-caption").insertAdjacentHTML(
  2593. "beforeend",
  2594. `<span class="label label-success"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> 磁力搜索</span><span class="label label-success"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> 自动去重</span>`
  2595. );
  2596. };
  2597.  
  2598. const magnets = await this.movieMagnet(this.params, start);
  2599. if (magnets?.length) this.refactorTd(magnets);
  2600. },
  2601. refactorTd(magnets) {
  2602. const table = DOC.querySelector(".x-table");
  2603. const caption = table.querySelector(".x-caption");
  2604.  
  2605. let subStart = () => {
  2606. caption.insertAdjacentHTML(
  2607. "beforeend",
  2608. `<span class="label label-success"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> 字幕筛选</span>`
  2609. );
  2610. };
  2611. let sortStart = () => {
  2612. caption.insertAdjacentHTML(
  2613. "beforeend",
  2614. `<span class="label label-success"><span class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span> 磁力排序</span>`
  2615. );
  2616. };
  2617.  
  2618. if (this.magnets) {
  2619. subStart = null;
  2620. sortStart = null;
  2621. magnets = unique([...this.magnets, ...magnets], "link");
  2622. }
  2623.  
  2624. table.querySelector("tfoot td").textContent = magnets.length;
  2625.  
  2626. magnets = this.movieSub(magnets, subStart);
  2627. magnets = this.movieSort(magnets, sortStart);
  2628. this.magnets = magnets;
  2629.  
  2630. magnets = this.createTd(magnets);
  2631. if (!magnets) return;
  2632. table.querySelector("tbody").innerHTML = magnets;
  2633.  
  2634. if (!subStart || !sortStart) return;
  2635. const node = table.querySelector("thead th:last-child");
  2636. node.innerHTML = `<a href="javascript:void(0);" title="复制所有磁力链接">全部复制</a>`;
  2637. node.querySelector("a").addEventListener("click", e => {
  2638. e.preventDefault();
  2639. e.stopPropagation();
  2640.  
  2641. GM_setClipboard(this.magnets.map(({ link }) => link).join("\n"));
  2642. const { target } = e;
  2643. target.textContent = "复制成功";
  2644.  
  2645. const timer = setTimeout(() => {
  2646. target.textContent = "全部复制";
  2647. clearTimeout(timer);
  2648. }, 300);
  2649. });
  2650. },
  2651. createTd(magnets) {
  2652. if (!magnets.length) return;
  2653. return magnets.reduce(
  2654. (acc, { name, link, size, date, from, href, zh }, index) => `
  2655. ${acc}
  2656. <tr>
  2657. <th scope="row">${index + 1}</th>
  2658. <th class="x-line" title="${name}">
  2659. <a href="${link}">${name}</a>
  2660. </th>
  2661. <td>${size}</td>
  2662. <td class="text-center">${date}</td>
  2663. <td class="text-center">
  2664. <a${href ? ` href="${href}" target="_blank" title="查看详情"` : ""}>
  2665. <code>${from ?? Domain}</code>
  2666. </a>
  2667. </td>
  2668. <td class="text-center">
  2669. <span
  2670. class="glyphicon ${
  2671. zh ? "glyphicon-ok-circle text-success" : "glyphicon-remove-circle text-danger"
  2672. }"
  2673. >
  2674. </span>
  2675. </td>
  2676. <td>
  2677. <a
  2678. href="javascript:void(0);"
  2679. data-copy="${link}"
  2680. class="x-mr"
  2681. title="复制磁力链接"
  2682. >
  2683. 复制链接
  2684. </a>
  2685. <a
  2686. hidden
  2687. href="javascript:void(0);"
  2688. data-magnet="${link}"
  2689. class="text-success"
  2690. title="仅添加离线任务"
  2691. >
  2692. 添加离线
  2693. </a>
  2694. </td>
  2695. </tr>`,
  2696. ""
  2697. );
  2698. },
  2699. async _driveOffline(e) {
  2700. await this.driveOffline(e, { ...this.params, magnets: this.magnets });
  2701. await delay(1);
  2702. this._driveMatch();
  2703. },
  2704. };
  2705. }
  2706.  
  2707. // javdb
  2708. class JavDB extends Common {
  2709. constructor() {
  2710. super();
  2711. return super.init();
  2712. }
  2713.  
  2714. excludeMenu = ["G_DARK", "L_MIT", "M_STAR", "M_SUB"];
  2715.  
  2716. routes = {
  2717. list: /^\/$|^\/(guess|censored|uncensored|western|fc2|anime|search|video_codes|tags|rankings|actors|series|makers|directors|publishers)/i,
  2718. movie: /^\/v\//i,
  2719. others: /.*/i,
  2720. };
  2721.  
  2722. // styles
  2723. _style = `
  2724. html {
  2725. padding: 0 !important;
  2726. overflow: overlay;
  2727. }
  2728. body {
  2729. padding-top: 3.25rem;
  2730. }
  2731. .section {
  2732. padding: 0;
  2733. }
  2734. section.section {
  2735. padding: 20px;
  2736. }
  2737. #search-type,
  2738. #video-search {
  2739. border: none;
  2740. }
  2741. #video-search:hover,
  2742. #video-search:focus {
  2743. z-index: auto;
  2744. }
  2745. .float-buttons {
  2746. right: 8px;
  2747. }
  2748. #footer,
  2749. nav.app-desktop-banner {
  2750. display: none !important;
  2751. }
  2752. [data-theme="dark"] ::-webkit-scrollbar-thumb {
  2753. background: #313131 !important;
  2754. }
  2755. [data-theme="dark"] img {
  2756. filter: brightness(0.9) contrast(0.9);
  2757. }
  2758. `;
  2759.  
  2760. // methods
  2761. _globalSearch = () => {
  2762. this.globalSearch("#video-search", "/search?q=%s");
  2763. };
  2764.  
  2765. // modules
  2766. list = {
  2767. docStart() {
  2768. const style = `
  2769. @media (max-width: 575.98px) {
  2770. .movie-list.v,
  2771. .actors {
  2772. grid-template-columns: repeat(2, minmax(0, 1fr));
  2773. }
  2774. .movie-list.h {
  2775. grid-template-columns: repeat(1, minmax(0, 1fr));
  2776. }
  2777. }
  2778. @media (min-width: 576px) {
  2779. .movie-list.v,
  2780. .actors {
  2781. grid-template-columns: repeat(3, minmax(0, 1fr));
  2782. }
  2783. .movie-list.h {
  2784. grid-template-columns: repeat(2, minmax(0, 1fr));
  2785. }
  2786. }
  2787. @media (min-width: 768px) {
  2788. .movie-list.v,
  2789. .actors {
  2790. grid-template-columns: repeat(4, minmax(0, 1fr));
  2791. }
  2792. .movie-list.h {
  2793. grid-template-columns: repeat(3, minmax(0, 1fr));
  2794. }
  2795. }
  2796. @media (min-width: 992px) {
  2797. .movie-list.v,
  2798. .actors {
  2799. grid-template-columns: repeat(5, minmax(0, 1fr));
  2800. }
  2801. .movie-list.h {
  2802. grid-template-columns: repeat(3, minmax(0, 1fr));
  2803. }
  2804. }
  2805. @media (min-width: 1200px) {
  2806. .movie-list.v,
  2807. .actors {
  2808. grid-template-columns: repeat(6, minmax(0, 1fr));
  2809. }
  2810. .movie-list.h {
  2811. grid-template-columns: repeat(4, minmax(0, 1fr));
  2812. }
  2813. }
  2814. @media (min-width: 1400px) {
  2815. .movie-list.v,
  2816. .actors {
  2817. grid-template-columns: repeat(7, minmax(0, 1fr));
  2818. }
  2819. .movie-list.h {
  2820. grid-template-columns: repeat(5, minmax(0, 1fr));
  2821. }
  2822. }
  2823. .movie-list,
  2824. .actors,
  2825. .section-container {
  2826. display: none;
  2827. gap: 20px;
  2828. margin: 0 !important;
  2829. padding: 0 0 20px;
  2830. }
  2831. .movie-list img,
  2832. .actors img,
  2833. .section-container img {
  2834. opacity: 0;
  2835. transition: opacity 0.25s linear !important;
  2836. }
  2837. .movie-list .box {
  2838. padding: 0 0 10px;
  2839. }
  2840. a.box:focus,
  2841. a.box:hover,
  2842. [data-theme="dark"] a.box:focus,
  2843. [data-theme="dark"] a.box:hover {
  2844. box-shadow: none !important;
  2845. }
  2846. [data-theme="dark"] .box:focus,
  2847. [data-theme="dark"] .box:hover {
  2848. background-color: #0a0a0a !important;
  2849. }
  2850. .movie-list .item .cover {
  2851. padding: 0 !important;
  2852. }
  2853. .movie-list.v .item .cover {
  2854. aspect-ratio: var(--x-thumb-ratio);
  2855. }
  2856. .movie-list.h .item .cover {
  2857. aspect-ratio: var(--x-cover-ratio);
  2858. }
  2859. .movie-list .item .cover img {
  2860. width: 100%;
  2861. height: 100%;
  2862. object-fit: contain;
  2863. }
  2864. .movie-list .item .cover:hover img {
  2865. z-index: 0;
  2866. transform: none;
  2867. }
  2868. .movie-list .item .video-title {
  2869. margin: 10px 10px 0;
  2870. padding: 0;
  2871. font-size: 14px;
  2872. line-height: var(--x-line-h);
  2873. }
  2874. .movie-list .item .score {
  2875. padding: 10px 10px 0;
  2876. }
  2877. .movie-list .item .meta {
  2878. padding: 4px 10px 0;
  2879. }
  2880. .movie-list .box .tags {
  2881. min-height: 36px;
  2882. margin-bottom: -8px;
  2883. padding: 4px 10px 0;
  2884. }
  2885. .movie-list .box .tags .tag {
  2886. margin-bottom: 8px;
  2887. }
  2888. .actors .box {
  2889. margin-bottom: 0;
  2890. padding-bottom: 10px;
  2891. font-size: 14px;
  2892. }
  2893. .actor-box a strong {
  2894. padding: 10px 10px 0;
  2895. line-height: unset;
  2896. }
  2897. .section-container .box {
  2898. font-size: 14px;
  2899. }
  2900. nav.pagination {
  2901. display: none;
  2902. margin: 0 -4px !important;
  2903. padding: 20px 0 40px;
  2904. }
  2905. nav.pagination,
  2906. :root[data-theme="dark"] nav.pagination {
  2907. border-top: none !important;
  2908. }
  2909. .awards {
  2910. padding-bottom: 20px;
  2911. }
  2912. .awards:last-child {
  2913. padding-bottom: 0;
  2914. }
  2915. `;
  2916. this.globalDark(`${this.style}${this.customStyle}${this._style}${style}${this.listMovieTitle()}`);
  2917. },
  2918. contentLoaded() {
  2919. this.captureJump();
  2920.  
  2921. this._globalSearch();
  2922. this.globalClick([".movie-list .box", ".actors .box a", ".section-container .box"], "", url => {
  2923. url = url.replace(location.origin, "");
  2924. const node = DOC.querySelector(`.movie-list .box[href="${url}"]`);
  2925. if (node) this.updateMatchStatus(node);
  2926. });
  2927.  
  2928. const selectors = [".movie-list", ".actors", ".section-container"];
  2929. if (DOC.querySelectorAll(selectors).length === 1) {
  2930. return selectors.forEach(item => this.modifyLayout(item));
  2931. }
  2932.  
  2933. GM_addStyle(`
  2934. .movie-list, .actors, .section-container { display: grid; }
  2935. .movie-list img, .actors img, .section-container img { opacity: 1; }
  2936. nav.pagination { display: flex; }
  2937. `);
  2938. },
  2939. captureJump() {
  2940. let { pathname, hash, search } = location;
  2941. if (pathname !== "/search" || hash !== "#jump") return;
  2942.  
  2943. let res = {};
  2944. search
  2945. .replace("?", "")
  2946. .split("&")
  2947. .forEach(item => {
  2948. const [key, val] = item.split("=");
  2949. res[key] = val;
  2950. });
  2951. res = res["q"];
  2952. if (!res) return;
  2953.  
  2954. const { regex } = codeParse(res);
  2955. const node = Array.from(DOC.querySelectorAll(".movie-list .item a") ?? []).find(item => {
  2956. return regex.test(item.querySelector(".video-title strong")?.textContent ?? "");
  2957. });
  2958. if (node?.href) location.replace(node.href);
  2959. },
  2960. modifyLayout(selectors) {
  2961. const container = DOC.querySelector(selectors);
  2962. if (!container) return;
  2963.  
  2964. const _container = container.cloneNode(true);
  2965. this.modifyItem(_container, selectors);
  2966. container.parentElement.replaceChild(_container, container);
  2967. _container.style.cssText += "display:grid";
  2968.  
  2969. const setSection = () => GM_addStyle(`section.section { padding-bottom: 0; }`);
  2970. const infScroll = this.listScroll(_container, "", ".pagination-next");
  2971. if (!infScroll) {
  2972. const pagination = DOC.querySelector("nav.pagination");
  2973. if (pagination) {
  2974. pagination.classList.add("x-flex");
  2975. setSection();
  2976. }
  2977. return;
  2978. }
  2979. setSection();
  2980.  
  2981. infScroll?.on("request", async (_, fetchPromise) => {
  2982. const { body } = await fetchPromise.then();
  2983. if (!body) return;
  2984. const items = this.modifyItem(body, selectors);
  2985. infScroll.appendItems(items);
  2986. });
  2987. },
  2988. modifyItem(container, selectors) {
  2989. const items = [];
  2990. container.querySelectorAll(`${selectors} a`).forEach(item => {
  2991. const _item = item.closest(`${selectors} > *`);
  2992. if (_item) {
  2993. this.modifyMovieBox(_item);
  2994. items.push(_item);
  2995. }
  2996. });
  2997. fadeInImg(items);
  2998. this._driveMatch(container);
  2999. return items;
  3000. },
  3001. modifyMovieBox(node = DOC) {
  3002. const items = node.querySelectorAll(".box");
  3003. for (const item of items) {
  3004. item?.querySelector(".video-title")?.classList.add("x-ellipsis", "x-title");
  3005. }
  3006. },
  3007. async _driveMatch(node = DOC) {
  3008. const items = node.querySelectorAll(".movie-list .box");
  3009. for (const item of items) await this.updateMatchStatus(item);
  3010. },
  3011. async updateMatchStatus(node) {
  3012. const code = node.querySelector(".video-title strong")?.textContent?.trim();
  3013. if (!code) return;
  3014.  
  3015. const res = await this.driveMatch({ code, res: "list" });
  3016. if (!res?.length) return;
  3017.  
  3018. const frame = node.querySelector(".cover");
  3019. frame.classList.add("x-player");
  3020. frame.setAttribute("title", "点击播放");
  3021. frame.setAttribute("data-code", res[0].pc);
  3022. node.querySelector(".x-title").classList.add("x-matched");
  3023. },
  3024. };
  3025. movie = {
  3026. params: {},
  3027. magnets: [],
  3028.  
  3029. docStart() {
  3030. const style = `
  3031. .first-block .copy-to-clipboard,
  3032. .review-buttons .panel-block:nth-child(2),
  3033. .top-meta > *:not(span.tag) {
  3034. display: none;
  3035. }
  3036. img {
  3037. width: 100% !important;
  3038. vertical-align: middle;
  3039. }
  3040. h2.title {
  3041. margin-bottom: 10px !important;
  3042. }
  3043. .video-meta-panel {
  3044. margin-bottom: 20px;
  3045. padding: 0;
  3046. }
  3047. .video-meta-panel > .columns {
  3048. position: relative;
  3049. margin: 0;
  3050. overflow: hidden;
  3051. }
  3052. @media screen and (min-width: 1024px) {
  3053. .video-meta-panel > .columns {
  3054. align-items: start;
  3055. }
  3056. }
  3057. .video-meta-panel > .columns > .column {
  3058. padding: 10px;
  3059. }
  3060. .column-video-cover {
  3061. position: relative;
  3062. margin: 10px;
  3063. padding: 0 !important;
  3064. overflow: hidden;
  3065. background-color: #000;
  3066. aspect-ratio: var(--x-cover-ratio);
  3067. }
  3068. @media only screen and (max-width: 1024px) {
  3069. .video-meta-panel .column-video-cover {
  3070. width: auto !important;
  3071. margin-bottom: 0;
  3072. }
  3073. #magnets-content > .columns {
  3074. padding: 0;
  3075. }
  3076. }
  3077. .column-video-cover .cover-container {
  3078. position: static !important;
  3079. display: inline !important;
  3080. }
  3081. .column-video-cover .cover-container::after {
  3082. height: 100%;
  3083. }
  3084. .column-video-cover .cover-container .play-button {
  3085. z-index: -1;
  3086. }
  3087. .x-contain.x-in + .play-button {
  3088. z-index: auto;
  3089. }
  3090. .preview-images img {
  3091. height: 100% !important;
  3092. object-fit: cover;
  3093. }
  3094. .column-video-cover a > img {
  3095. max-height: unset;
  3096. opacity: 0;
  3097. }
  3098. .x-contain {
  3099. position: absolute;
  3100. top: 0;
  3101. left: 0;
  3102. z-index: -1;
  3103. width: 100% !important;
  3104. height: 100% !important;
  3105. object-fit: contain !important;
  3106. border: none;
  3107. opacity: 0;
  3108. }
  3109. .x-contain.x-in {
  3110. z-index: auto;
  3111. display: block !important;
  3112. }
  3113. .movie-panel-info div.panel-block {
  3114. padding: 10px 0;
  3115. font-size: 14px;
  3116. }
  3117. .movie-panel-info > div.panel-block:first-child {
  3118. padding-top: 0;
  3119. }
  3120. .movie-panel-info > div.panel-block:last-child {
  3121. padding-bottom: 0;
  3122. }
  3123. .video-detail > .columns {
  3124. margin: 0 0 20px;
  3125. }
  3126. .video-detail > .columns > .column {
  3127. padding: 0;
  3128. }
  3129. .message-body {
  3130. padding: 10px;
  3131. }
  3132. .video-panel .tile-images {
  3133. gap: 10px;
  3134. }
  3135. @media (max-width: 575.98px) {
  3136. .video-panel .tile-images {
  3137. grid-template-columns: repeat(2, minmax(0, 1fr));
  3138. }
  3139. }
  3140. @media (min-width: 576px) {
  3141. .video-panel .tile-images {
  3142. grid-template-columns: repeat(3, minmax(0, 1fr));
  3143. }
  3144. }
  3145. @media (min-width: 768px) {
  3146. .video-panel .tile-images {
  3147. grid-template-columns: repeat(4, minmax(0, 1fr));
  3148. }
  3149. }
  3150. @media (min-width: 992px) {
  3151. .video-panel .tile-images {
  3152. grid-template-columns: repeat(5, minmax(0, 1fr));
  3153. }
  3154. }
  3155. @media (min-width: 1200px) {
  3156. .video-panel .tile-images {
  3157. grid-template-columns: repeat(6, minmax(0, 1fr));
  3158. }
  3159. }
  3160. @media (min-width: 1400px) {
  3161. .video-panel .tile-images {
  3162. grid-template-columns: repeat(7, minmax(0, 1fr));
  3163. }
  3164. }
  3165. .preview-video-container::after {
  3166. height: 100%;
  3167. }
  3168. .preview-images > a {
  3169. aspect-ratio: var(--x-sprite-ratio);
  3170. }
  3171. #magnets > .message {
  3172. margin-bottom: 0;
  3173. }
  3174. .top-meta {
  3175. padding: 0 !important;
  3176. }
  3177. .top-meta > span.tag {
  3178. margin: 0 5px 10px 0;
  3179. }
  3180. #magnets-content {
  3181. max-height: 400px;
  3182. overflow: auto;
  3183. }
  3184. #magnets-content > .columns {
  3185. margin: 0;
  3186. padding: 5px 0;
  3187. }
  3188. #magnets-content > .columns > .column {
  3189. display: flex;
  3190. align-items: center;
  3191. margin: 0;
  3192. padding: 5px 10px;
  3193. }
  3194. #magnets-content .tag,
  3195. #magnets-content .button {
  3196. margin: 0;
  3197. }
  3198. .review-items .review-item {
  3199. padding: 10px 0;
  3200. }
  3201. .review-items .review-item:first-child {
  3202. padding-top: 0;
  3203. }
  3204. .review-items .review-item:last-child {
  3205. padding-bottom: 0;
  3206. }
  3207. .message-header {
  3208. padding: 8px 10px;
  3209. }
  3210. .tile-images.tile-small .tile-item {
  3211. padding-bottom: 10px;
  3212. background-color: #fff;
  3213. }
  3214. [data-theme="dark"] .tile-images.tile-small .tile-item {
  3215. background-color: #0a0a0a;
  3216. }
  3217. .tile-images.tile-small .tile-item img {
  3218. margin-bottom: 10px;
  3219. }
  3220. .tile-images.tile-small .tile-item > div {
  3221. padding: 0 10px !important;
  3222. }
  3223. #x-switch {
  3224. display: none;
  3225. }
  3226. #x-switch > * {
  3227. flex: 1;
  3228. }
  3229. .x-from {
  3230. min-width: 70px;
  3231. }
  3232. .x-offline {
  3233. width: 100%;
  3234. }
  3235. `;
  3236. this.globalDark(`${this.style}${this.customStyle}${this._style}${style}`);
  3237. },
  3238. contentLoaded() {
  3239. this._globalSearch();
  3240. this.globalClick([".tile-images.tile-small a.tile-item"]);
  3241.  
  3242. const preview = DOC.querySelector(".preview-images");
  3243. if (preview && !preview.querySelector("a")) preview.closest(".columns").remove();
  3244.  
  3245. this.params = this.getParams();
  3246.  
  3247. addCopyTarget("h2.title", { title: "复制标题" });
  3248. addCopyTarget(".first-block .value", { title: "复制番号" });
  3249. this._movieJump();
  3250.  
  3251. this.initSwitch();
  3252. this.updateSwitch({ key: "img", title: "大图" });
  3253. this.updateSwitch({ key: "video", title: "预览" });
  3254. this.updateSwitch({ key: "player", title: "视频", type: "video" });
  3255.  
  3256. this._movieTitle();
  3257. this._movieMagnet();
  3258. this._driveMatch();
  3259. DOC.querySelector(".x-offline")?.addEventListener("click", e => this._driveOffline(e));
  3260. },
  3261. getParams() {
  3262. const infos = Array.from(DOC.querySelectorAll(".movie-panel-info > .panel-block") ?? []);
  3263. const findInfos = label => {
  3264. return (
  3265. infos
  3266. .find(info => info.querySelector("strong")?.textContent === label)
  3267. ?.querySelector(".value")
  3268. ?.textContent?.trim() ?? ""
  3269. );
  3270. };
  3271.  
  3272. return {
  3273. title: DOC.querySelector("h2.title").textContent.trim(),
  3274. code: DOC.querySelector(".first-block .value").textContent.trim(),
  3275. date: findInfos("日期:"),
  3276. studio: findInfos("片商:"),
  3277. };
  3278. },
  3279. _movieJump() {
  3280. const { code } = this.params;
  3281. if (code.startsWith("FC2")) return;
  3282.  
  3283. const node = DOC.querySelector(".first-block .value");
  3284. const prefix = node.querySelector("a")?.textContent ?? "";
  3285. if (!prefix || prefix === "复制") return;
  3286.  
  3287. const start = () => {
  3288. node.insertAdjacentHTML(
  3289. "beforeend",
  3290. `<a class="x-ml" href="https://www.javbus.com/${code}" title="跳转 JavBus">跳转</a>`
  3291. );
  3292. };
  3293. this.movieJump(start);
  3294. },
  3295. initSwitch() {
  3296. const info = DOC.querySelector(".movie-panel-info");
  3297. info.insertAdjacentHTML(
  3298. "afterbegin",
  3299. `<div class="panel-block" id="x-switch">
  3300. <a class="button is-small is-light is-active" for="x-switch-cover" title="点击放大或切换静音">封面</a>
  3301. </div>`
  3302. );
  3303. const cover = DOC.querySelector(".column-video-cover a > img");
  3304. cover.id = "x-switch-cover";
  3305. cover.classList.add("x-contain", "x-in");
  3306.  
  3307. DOC.querySelector("#x-switch").addEventListener("click", ({ target }) => {
  3308. const { classList } = target;
  3309. if (
  3310. target.nodeName !== "A" ||
  3311. target.getAttribute("disabled") === "" ||
  3312. classList.contains("is-loading")
  3313. ) {
  3314. return;
  3315. }
  3316.  
  3317. const id = target.getAttribute("for");
  3318. const item = DOC.querySelector(`#${id}`);
  3319.  
  3320. if (classList.contains("is-active")) {
  3321. item.parentNode.click();
  3322. item.muted = !item.muted;
  3323. } else {
  3324. const preItem = DOC.querySelector(".column-video-cover .x-contain.x-in");
  3325. preItem?.pause && preItem.pause();
  3326. preItem.classList.toggle("x-in");
  3327. item.classList.toggle("x-in");
  3328. item?.play && item.play();
  3329. item?.focus && item.focus();
  3330.  
  3331. const preTarget = DOC.querySelector("#x-switch a.is-active");
  3332. preTarget.removeAttribute("title");
  3333. preTarget.classList.toggle("is-active");
  3334. target.classList.toggle("is-active");
  3335. target.setAttribute("title", "点击放大或切换静音");
  3336. }
  3337. });
  3338. },
  3339. async updateSwitch({ key, title, type }) {
  3340. if (!type) type = key;
  3341. const id = `x-switch-${key}`;
  3342. const switcher = DOC.querySelector("#x-switch");
  3343.  
  3344. const start = () => {
  3345. if (!switcher.classList.contains("x-flex")) switcher.classList.add("x-flex");
  3346. switcher.insertAdjacentHTML(
  3347. "beforeend",
  3348. `<a class="button is-small is-light is-loading" for="${id}">查看${title}</a>`
  3349. );
  3350. };
  3351.  
  3352. const src = await this[`movie${key[0].toUpperCase()}${key.slice(1)}`](this.params, start);
  3353. const node = switcher.querySelector(`a[for="${id}"]`);
  3354. if (!node) return;
  3355.  
  3356. node.classList.remove("is-loading");
  3357. if (!src?.length) {
  3358. node.setAttribute("disabled", "");
  3359. node.textContent = `暂无${title}`;
  3360. return;
  3361. }
  3362.  
  3363. let item = DOC.create(type, { id, class: "x-contain" });
  3364. if (typeof src === "string") item.src = src;
  3365.  
  3366. if (type === "video") {
  3367. if (Object.prototype.toString.call(src) === "[object Array]") {
  3368. src.forEach(params => {
  3369. const source = DOC.create("source", params);
  3370. item.appendChild(source);
  3371. });
  3372. }
  3373.  
  3374. item.controls = true;
  3375. item.currentTime = 3;
  3376. item.muted = true;
  3377. item.preload = "metadata";
  3378. item.addEventListener("click", e => {
  3379. e.preventDefault();
  3380. e.stopPropagation();
  3381. const { target: video } = e;
  3382. video.paused ? video.play() : video.pause();
  3383. });
  3384. } else {
  3385. item = DOC.create("a", { "data-fancybox": "gallery", href: item.src }, item);
  3386. }
  3387.  
  3388. DOC.querySelector(".column-video-cover").insertAdjacentElement("beforeend", item);
  3389. },
  3390. async _movieTitle() {
  3391. const start = () => {
  3392. DOC.querySelector("#x-switch").insertAdjacentHTML(
  3393. "afterend",
  3394. `<div class="panel-block"><strong>机翻:</strong>&nbsp;<span class="value x-transTitle">查询中...</span></div>`
  3395. );
  3396. };
  3397.  
  3398. const transTitle = await this.movieTitle(this.params, start);
  3399. const transTitleNode = DOC.querySelector(".x-transTitle");
  3400. if (transTitleNode) transTitleNode.textContent = transTitle ?? "查询失败";
  3401. },
  3402. async _driveMatch() {
  3403. const start = () => {
  3404. if (DOC.querySelector(".x-res")) return;
  3405.  
  3406. GM_addStyle(`#magnets-content button.button.x-hide{ display: flex; }`);
  3407. DOC.querySelector(".movie-panel-info").insertAdjacentHTML(
  3408. "beforeend",
  3409. `<div class="panel-block"><strong>资源:</strong>&nbsp;<span class="value x-res">查询中...</span></div><div class="panel-block"><button class="button is-info is-small x-offline" data-magnet="all">一键离线</button></div>`
  3410. );
  3411. };
  3412.  
  3413. const res = await this.driveMatch(this.params, start);
  3414. const resNode = DOC.querySelector(".x-res");
  3415. if (!resNode) return;
  3416.  
  3417. resNode.innerHTML = !res?.length
  3418. ? "暂无网盘资源"
  3419. : res.reduce(
  3420. (acc, { pc, t, n }) =>
  3421. `${acc}<div class="x-ellipsis"><a href="${this.pcUrl}${pc}" target="_blank" title="${t} / ${n}">${n}</a></div>`,
  3422. ""
  3423. );
  3424. },
  3425. async _movieMagnet() {
  3426. const start = () => {
  3427. const node = DOC.querySelector(".top-meta");
  3428. if (!node) return;
  3429. node.insertAdjacentHTML(
  3430. "beforeend",
  3431. `<span class="tag is-success">磁力搜索</span><span class="tag is-success">自动去重</span>`
  3432. );
  3433. };
  3434. let magnets = (await this.movieMagnet(this.params, start)) ?? [];
  3435. const curMagnets = Array.from(DOC.querySelectorAll("#magnets-content .item") ?? []).map(item => {
  3436. const name = item.querySelector(".magnet-name");
  3437. const size = name.querySelector(".meta")?.textContent.split(",")[0].trim() ?? "";
  3438. return {
  3439. bytes: transToBytes(size),
  3440. date: item.querySelector(".date .time").textContent,
  3441. from: Domain,
  3442. link: name.querySelector("a").href.split("&")[0],
  3443. name: name.querySelector(".name").textContent,
  3444. size,
  3445. zh: !!name?.querySelector(".tags .tag.is-warning.is-small.is-light"),
  3446. };
  3447. });
  3448. magnets = unique([...curMagnets, ...magnets], "link");
  3449. this._movieSort(magnets);
  3450. },
  3451. _movieSort(magnets) {
  3452. const start = () => {
  3453. const node = DOC.querySelector(".top-meta");
  3454. if (node) node.insertAdjacentHTML("beforeend", `<span class="tag is-success">磁力排序</span>`);
  3455. };
  3456. magnets = this.movieSort(magnets, start);
  3457. if (!magnets.length) return;
  3458. this.magnets = magnets;
  3459.  
  3460. magnets = magnets.map(
  3461. ({ link, name, size, zh, date, from, href }, index) => `
  3462. <div class="item columns is-desktop${(index + 1) % 2 === 0 ? "" : " odd"}">
  3463. <div class="magnet-name column is-four-fifths" title="${name}">
  3464. <a href="${link}" class="x-ellipsis">
  3465. <span class="name">${name}</span>
  3466. </a>
  3467. </div>
  3468. <div class="column">
  3469. <span class="tag is-warning is-small is-light ${zh ? "" : " x-out"}">字幕</span>
  3470. </div>
  3471. <div class="date column">
  3472. <span>${size}</span>
  3473. </div>
  3474. <div class="date column">
  3475. <span>${date}</span>
  3476. </div>
  3477. <div class="column">
  3478. <a
  3479. class="tag is-danger is-small is-light x-from"
  3480. ${href ? `href="${href}" target="_blank" title="查看详情"` : ""}
  3481. >
  3482. ${from}
  3483. </a>
  3484. </div>
  3485. <div class="buttons column">
  3486. <button class="button is-info is-small" data-copy="${link}" title="复制磁力链接">复制链接</button><button class="button is-info is-small x-ml x-hide" data-magnet="${link}" title="仅添加离线任务">添加离线</button>
  3487. </div>
  3488. </div>
  3489. `
  3490. );
  3491. magnets = magnets.join("");
  3492. const node = DOC.querySelector("#magnets-content");
  3493. node.innerHTML = magnets;
  3494.  
  3495. DOC.querySelector("#magnets-content").addEventListener("click", e => {
  3496. !handleCopyTxt(e, "复制成功") && this._driveOffline(e);
  3497. });
  3498. },
  3499. async _driveOffline(e) {
  3500. await this.driveOffline(e, { ...this.params, magnets: this.magnets });
  3501. await delay(1);
  3502. this._driveMatch();
  3503. },
  3504. };
  3505. others = {
  3506. docStart() {
  3507. GM_addStyle(`${this.style}${this._style}`);
  3508. },
  3509. contentLoaded() {
  3510. this._globalSearch();
  3511. },
  3512. };
  3513. }
  3514.  
  3515. // 115
  3516. class Drive115 {
  3517. contentLoaded() {
  3518. window.focus();
  3519. DOC.querySelector(`#js_ver_code_box button[rel="verify"]`).addEventListener("click", () => {
  3520. const interval = setInterval(() => {
  3521. if (DOC.querySelector(".vcode-hint").getAttribute("style").indexOf("none") !== -1) {
  3522. clearTimer();
  3523. window.open("", "_self");
  3524. window.close();
  3525. }
  3526. }, 300);
  3527.  
  3528. const timeout = setTimeout(() => clearTimer(), 600);
  3529.  
  3530. const clearTimer = () => {
  3531. clearInterval(interval);
  3532. clearTimeout(timeout);
  3533. };
  3534. });
  3535. }
  3536. }
  3537.  
  3538. try {
  3539. const Process = eval(`new ${Domain}()`);
  3540. Process.docStart && Process.docStart();
  3541. Process.contentLoaded && DOC.addEventListener("DOMContentLoaded", () => Process.contentLoaded());
  3542. Process.load && window.addEventListener("load", () => Process.load());
  3543. } catch (err) {
  3544. console.error(`${GM_info.script.name}: 无匹配模块`);
  3545. }
  3546. })();