JavBus工具

暗黑模式、滚动加载、预览视频、预览大图、网盘资源匹配、离线下载(115)...

اعتبارا من 20-12-2021. شاهد أحدث إصدار.

  1. // ==UserScript==
  2. // @name JavBus工具
  3. // @namespace https://greasyfork.org/users/175514
  4. // @description 暗黑模式、滚动加载、预览视频、预览大图、网盘资源匹配、离线下载(115)...
  5. // @version 0.3.1
  6. // @icon https://z3.ax1x.com/2021/10/15/53gMFS.png
  7. // @include *://*.javbus.com/*
  8. // @include *://*.115.com/*
  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. // @require https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js
  12. // @resource fail https://z3.ax1x.com/2021/10/15/53gcex.png
  13. // @resource info https://z3.ax1x.com/2021/10/15/53g2TK.png
  14. // @resource success https://z3.ax1x.com/2021/10/15/53gqTf.png
  15. // @run-at document-start
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_getResourceURL
  18. // @grant GM_xmlhttpRequest
  19. // @grant GM_setClipboard
  20. // @grant GM_notification
  21. // @grant GM_openInTab
  22. // @grant GM_addStyle
  23. // @grant GM_setValue
  24. // @grant GM_getValue
  25. // @grant GM_info
  26. // @connect *
  27. // @license MIT
  28. // ==/UserScript==
  29.  
  30. /**
  31. * 非完全测试,自用Edge
  32. * TODO:
  33. * ✔️ 暗黑模式
  34. * ✔️ 点击事件(新窗口打开,左键前台,右键后台)
  35. * ✔️ 滚动加载
  36. * ✔️ 获取预览大图(javstore.net)
  37. * ✔️ 获取预览视频(r18.com),如有资源点击封面图或样品图像处进行预览
  38. * ✔️ 获取演员名单(javdb.com)
  39. * ✔️ 离线下载(请求离线成功2秒后查询结果汇报),自动修改文件名称,(如已设置操作目录cid,自动移动视频文件至对应目录并删除原目录)
  40. * ✔️ 一键离线(字幕>尺寸>日期)排队执行离线下载,成功一结束排队
  41. * ✔️ 115账号验证弹窗
  42. * ✔️ 查询115是否已有离线资源并缓存(默认3天有效),3天后重新查询,支持手动刷新“资源刷新”
  43. * ✔️ 根据缓存资源数据匹配列表页已有资源状态,添加已有资源列表页
  44. * ✔️ 支持手动获取操作目录(需设置)下列表数据,自动过滤(视频文件 & >100m)以缓存,匹配列表页(注:网盘列表数据与资源数据并不叠加)
  45. */
  46.  
  47. (function () {
  48. const dm = GM_getValue("dm") ?? matchMedia("(prefers-color-scheme: dark)").matches;
  49. const lm = GM_getValue("lm") ?? false;
  50. const ck = GM_getValue("ck") ?? false;
  51.  
  52. const rootId = GM_getValue("rid") ?? "";
  53. const rDate = GM_getValue("rDate") ?? "Unknown";
  54. const rSize = GM_getValue("rSize") ?? 0;
  55.  
  56. const SUFFIX = " !important";
  57. const { host, pathname } = location;
  58. let mousePoint = 0;
  59.  
  60. const insertBefore = (el, htmlString) => el.insertAdjacentHTML("beforebegin", htmlString);
  61. const insertAfter = (el, htmlString) => el.insertAdjacentHTML("afterend", htmlString);
  62. const appendBefore = (el, htmlString) => el.insertAdjacentHTML("afterbegin", htmlString);
  63. const appendAfter = (el, htmlString) => el.insertAdjacentHTML("beforeend", htmlString);
  64. const doc = document;
  65. doc.create = (tag, attr = {}, child = "") => {
  66. if (!tag) return null;
  67. tag = doc.createElement(tag);
  68. Object.keys(attr).forEach(name => tag.setAttribute(name, attr[name]));
  69. typeof child === "string" && tag.appendChild(doc.createTextNode(child));
  70. typeof child === "object" && tag.appendChild(child);
  71. return tag;
  72. };
  73.  
  74. let rd;
  75. const lf = localforage;
  76. lf.upItem = async (key, val) => {
  77. const original = (await lf.getItem(key)) ?? {};
  78. await lf.setItem(key, Object.assign(original, val));
  79. };
  80.  
  81. const request = (url, method = "GET", data = {} | "", params = {}) => {
  82. if (!url) return;
  83. method = method?.toLocaleUpperCase() ?? "GET";
  84. if (method === "POST") {
  85. const cType = { "Content-Type": "application/x-www-form-urlencoded" };
  86. params.headers = Object.assign(params.headers ?? {}, cType);
  87. }
  88. if (typeof data === "object") {
  89. const keys = Object.keys(data);
  90. if (keys.length) {
  91. data = keys.reduce((pre, cur) => `${pre}${pre ? "&" : ""}${cur}=${encodeURIComponent(data[cur])}`, "");
  92. if (method === "GET") url = `${url}?${data}`;
  93. }
  94. }
  95. return new Promise(resolve => {
  96. GM_xmlhttpRequest({
  97. url,
  98. method,
  99. data,
  100. timeout: 20000,
  101. onload: ({ status, responseText }) => {
  102. if (status === 404) resolve(false);
  103. if (/<\/?[a-z][\s\S]*>/i.test(responseText)) {
  104. responseText = new DOMParser().parseFromString(responseText, "text/html");
  105. }
  106. if (/^{.*}$/.test(responseText)) {
  107. responseText = JSON.parse(responseText);
  108. }
  109. if (`${responseText?.errcode}` === "911") verify();
  110. resolve(responseText);
  111. },
  112. ...params,
  113. });
  114. });
  115. };
  116. const notifiy = (title = "" | {}, text = "", image = "info", clickUrl = "") => {
  117. let params = {
  118. title,
  119. text: text ?? GM_info.script.name,
  120. image,
  121. highlight: true,
  122. timeout: 3000,
  123. clickUrl,
  124. };
  125. if (typeof title === "object") params = { ...params, ...title };
  126. if (params.clickUrl) params.onclick = () => GM_openInTab(clickUrl, { active: true });
  127. GM_notification({ ...params, image: GM_getResourceURL(params?.image ?? "info") });
  128. };
  129. const verify = async () => {
  130. const time = new Date().getTime();
  131. const h = 667;
  132. const w = 375;
  133. const t = (window.screen.availHeight - h) / 2;
  134. const l = (window.screen.availWidth - w) / 2;
  135. window.open(
  136. `https://captchaapi.115.com/?ac=security_code&type=web&cb=Close911_${time}`,
  137. "验证账号",
  138. `height=${h},width=${w},top=${t},left=${l},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
  139. );
  140. };
  141. const handleGetRes = async () => {
  142. const mask = doc.querySelector("#res-mask");
  143. const title = mask.querySelector(".res-title");
  144. const content = mask.querySelector(".res-content");
  145. const btn = mask.querySelector("button");
  146. // start
  147. title.textContent = "正在获取网盘列表数据";
  148. content.textContent = "正在获取:1页";
  149. btn.textContent = "请求中...";
  150. btn.classList.toggle("disabled");
  151. // change
  152. const change = ({ page, pages, data, count }) => {
  153. content.textContent = `正在获取:${page}/${pages}页,获取数据:${data.length}/${count}`;
  154. };
  155. // done
  156. const res = await getRes(change);
  157. notifiy(res);
  158. title.textContent = res.title;
  159. content.textContent = res.text;
  160. btn.textContent = res.err ? "重新获取" : "处理中...";
  161. if (res.err) return btn.classList.toggle("disabled");
  162. // filter
  163. const limit = 100;
  164. const data = res.data
  165. .filter(({ play_long, s }) => play_long && s && play_long > 0 && s >= limit * 1024 * 1024)
  166. .map(item => {
  167. return {
  168. fid: item.fid,
  169. cid: item.cid,
  170. link: `https://v.anxia.com/?pickcode=${item.pc}`,
  171. getDate: item.t,
  172. name: item.n,
  173. timestamp: item.te || item.tp,
  174. };
  175. });
  176. // setValue
  177. title.textContent = "正在清除旧缓存数据";
  178. btn.textContent = "清除中...";
  179. await rd.clear();
  180. title.textContent = "正在设置缓存数据";
  181. btn.textContent = "缓存中...";
  182. const len = data.length;
  183. for (let index = 0; index < len; index++) {
  184. const item = data[index];
  185. await rd.setItem(item.fid, item);
  186. }
  187. GM_setValue("rDate", getDate());
  188. GM_setValue("rSize", len);
  189. // end
  190. title.textContent = `数据缓存已完成(${len})`;
  191. let second = 3;
  192. btn.textContent = `立即刷新(${second}s)`;
  193. btn.classList.toggle("disabled");
  194. btn.removeEventListener("click", handleGetRes);
  195. btn.addEventListener("click", () => location.reload());
  196. setInterval(() => {
  197. second--;
  198. btn.textContent = `立即刷新(${second}s)`;
  199. if (second === 0) location.reload();
  200. }, 1000);
  201. };
  202. const getRes = async change => {
  203. const re = {
  204. title: "获取失败,请稍后重试",
  205. text: "接口失效或网盘未登录,及确保网络状态",
  206. image: "fail",
  207. err: true,
  208. data: [],
  209. page: 1,
  210. pages: "Unknown",
  211. count: "Unknown",
  212. };
  213.  
  214. const URL = "https://webapi.115.com/files";
  215. const limit = 115;
  216. const params = {
  217. aid: 1,
  218. cid: rootId,
  219. o: "user_ptime",
  220. asc: 0,
  221. offset: 0,
  222. show_dir: 1,
  223. limit,
  224. code: "",
  225. scid: "",
  226. snap: 0,
  227. natsort: 1,
  228. record_open_time: 1,
  229. source: "",
  230. format: "json",
  231. type: 4,
  232. star: "",
  233. suffix: "",
  234. };
  235.  
  236. let res = await request(URL, "GET", params);
  237. if (!res?.count || !res?.data?.length) return re;
  238. re.err = false;
  239. let { count, data } = res;
  240. re.count = count;
  241. re.data = data;
  242. const pages = Math.ceil(count / limit);
  243. re.pages = pages;
  244. change(re);
  245. for (let index = 1; index < pages; index++) {
  246. params.offset = index * limit;
  247. re.page = index + 1;
  248. change(re);
  249. res = await request(URL, "GET", params);
  250. if (!res?.data?.length) continue;
  251. data = [...data, ...res.data];
  252. re.data = data;
  253. change(re);
  254. }
  255. re.title = `获取完成(${((data.length / count) * 100).toFixed(0)}%)`;
  256. re.text = "将进行数据过滤以缓存,完成后自动刷新页面";
  257. re.image = "success";
  258. return re;
  259. };
  260. const handleClick = (selectors, node = doc) => {
  261. for (const item of node.querySelectorAll(selectors)) {
  262. const href = item?.href;
  263. if (!href) continue;
  264. item.addEventListener("contextmenu", e => e.preventDefault());
  265. item.addEventListener("click", e => {
  266. e.preventDefault();
  267. GM_openInTab(href, { active: true });
  268. });
  269. item.addEventListener("mousedown", e => {
  270. if (e.button !== 2) return;
  271. e.preventDefault();
  272. mousePoint = e.screenX + e.screenY;
  273. });
  274. item.addEventListener("mouseup", e => {
  275. const num = e.screenX + e.screenY - mousePoint;
  276. if (e.button !== 2 || num > 5 || num < -5) return;
  277. e.preventDefault();
  278. GM_openInTab(href);
  279. });
  280. }
  281. };
  282. const getDate = (str = "") => {
  283. const date = str ? new Date(str) : new Date();
  284. const Y = date.getFullYear();
  285. const M = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
  286. const D = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
  287. return `${Y}-${M}-${D}`;
  288. };
  289. const getCodes = code => [
  290. code,
  291. code.replace(/-/g, ""),
  292. code.replace(/-/g, "-0"),
  293. code.replace(/-/g, "0"),
  294. code.replace(/-/g, "00"),
  295. code.replace(/-/g, "_"),
  296. code.replace(/-/g, "_0"),
  297. code.replace(/-0/g, ""),
  298. code.replace(/-0/g, "-"),
  299. code.replace(/-0/g, "00"),
  300. ];
  301. const scriptStart = () => {
  302. doc.addEventListener("keyup", event => {
  303. const e = event || window.event;
  304. if (e && e.keyCode === 191 && !["INPUT", "TEXTAREA"].includes(doc.activeElement.nodeName)) {
  305. doc.querySelector("#search-input").focus();
  306. }
  307. });
  308.  
  309. if (rootId) {
  310. const mask = doc.create("div", { id: "res-mask", class: "mask" });
  311. GM_addStyle(`
  312. #res-box { color: ${LabelColor}; text-align: center; }
  313. #res-box span, .res-title { font-size: 24px; }
  314. .res-desc { margin-bottom: 8px; }
  315. .res-content { color: ${SecondaryLabelColor}; }
  316. .res-action { margin-top: 15px; }
  317. `);
  318. appendAfter(
  319. mask,
  320. `<div id="res-box">
  321. <span class="glyphicon glyphicon-cloud" aria-hidden="true"></span>
  322. <div class="res-title">获取网盘列表数据</div>
  323. <div class="res-desc">(cid: ${rootId})</div>
  324. <div class="res-content">上次获取:${rDate},缓存数据:${rSize}</div>
  325. <div class="res-action">
  326. <button class="btn btn-default">开始获取</button>
  327. </div>
  328. </div>`
  329. );
  330. mask.querySelector("button").addEventListener("click", handleGetRes);
  331. doc.body.appendChild(mask);
  332. }
  333.  
  334. const nav = `<a href="https://www.javbus.com/resource">资源</a>`;
  335. let navbar = doc.querySelector("#navbar .nav.navbar-nav");
  336. if (navbar) return appendAfter(navbar, `<li class="resource">${nav}</li>`);
  337. navbar = doc.querySelector("#toptb .wp .z ul");
  338. if (navbar) return appendAfter(navbar, `<li class="nav-title nav-inactive">${nav}</li>`);
  339. };
  340.  
  341. const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  342.  
  343. class Common {
  344. docStart = () => {};
  345. contentLoaded = () => {};
  346. load = () => {};
  347. }
  348. class Waterfall extends Common {
  349. isHome = /^(\/(page\/\d+)?|\/uncensored(\/page\/\d+)?)+$/i.test(pathname);
  350. docStart = () => {
  351. GM_addStyle(`
  352. .search-header {
  353. padding: 0${SUFFIX};
  354. background: none${SUFFIX};
  355. box-shadow: none${SUFFIX};
  356. }
  357. .photo-frame {
  358. position: relative;
  359. margin: 10px${SUFFIX};
  360. }
  361. .photo-frame img {
  362. height: 100%${SUFFIX};
  363. width: 100%${SUFFIX};
  364. object-fit: cover${SUFFIX};
  365. margin: 0${SUFFIX};
  366. }
  367. .photo-info { padding: 10px${SUFFIX}; }
  368. .alert-page { margin: 20px${SUFFIX}; }
  369. `);
  370. if (!lm) return;
  371. const itemSizer = `167px`;
  372. const gutterSizer = `20px`;
  373. GM_addStyle(`
  374. .pagination, footer { display: none${SUFFIX}; }
  375. .page-load-status {
  376. display: none;
  377. padding-bottom: ${gutterSizer};
  378. text-align: center;
  379. }
  380. body { overflow: hidden; }
  381. .scrollBox {
  382. height: calc(100vh - 50px);
  383. overflow: hidden scroll;
  384. }
  385. #waterfall {
  386. opacity: 0;
  387. margin: ${gutterSizer} auto 0 auto${SUFFIX};
  388. }
  389. .item-sizer, .item a { width: ${itemSizer}${SUFFIX}; }
  390. .gutter-sizer { width: ${gutterSizer}${SUFFIX}; }
  391. .item a { margin: 0 0 ${gutterSizer} 0${SUFFIX}; }
  392. `);
  393. };
  394. contentLoaded = () => {
  395. const nav = doc.querySelector(".search-header .nav");
  396. if (nav) nav.setAttribute("class", "nav nav-pills");
  397. if (!lm) return this.modifyItem();
  398. this.handleLoadMore();
  399. if (!this.isHome) this.modifyLayout();
  400. };
  401. load = () => {
  402. if (lm && this.isHome) this.modifyLayout();
  403. };
  404. handleLoadMore = () => {
  405. const oldWaterfall = doc.querySelector("#waterfall");
  406. if (!oldWaterfall) return GM_addStyle(`#waterfall { opacity: 1; }`);
  407. const newWaterfall = doc.querySelector("#waterfall #waterfall");
  408. if (newWaterfall) oldWaterfall.parentNode.replaceChild(newWaterfall, oldWaterfall);
  409.  
  410. const waterfall = doc.querySelector("#waterfall");
  411. appendBefore(waterfall, `<div class="item-sizer"></div><div class="gutter-sizer"></div>`);
  412. insertAfter(
  413. waterfall,
  414. `<div class="page-load-status"><span class="loader-ellips infinite-scroll-request">Loading...</span><span class="infinite-scroll-last">End of content</span><span class="infinite-scroll-error">No more pages to load</span></div>`
  415. );
  416.  
  417. doc.querySelector(".container-fluid .row").classList.add("scrollBox");
  418. };
  419. modifyLayout = () => {
  420. const waterfall = doc.querySelector("#waterfall");
  421. if (!waterfall) return;
  422. let msnry = new Masonry(waterfall, {
  423. itemSelector: "none",
  424. columnWidth: ".item-sizer",
  425. gutter: ".gutter-sizer",
  426. horizontalOrder: true,
  427. fitWidth: true,
  428. stagger: 30,
  429. visibleStyle: { transform: "translateY(0)", opacity: 1 },
  430. hiddenStyle: { transform: "translateY(120px)", opacity: 0 },
  431. });
  432. imagesLoaded(waterfall, () => {
  433. msnry.options.itemSelector = ".item";
  434. const elems = waterfall.querySelectorAll(".item");
  435. this.modifyItem();
  436. msnry.appended(elems);
  437. GM_addStyle(`#waterfall { opacity: 1; }`);
  438. });
  439. // 搜索页滚动加载需特殊处理
  440. const path = !/^\/(uncensored\/)?(search|searchstar)+\//i.test(pathname)
  441. ? "#next"
  442. : function () {
  443. const items = ["search", "searchstar"];
  444. for (const item of items) {
  445. if (pathname.indexOf(`${item}/`) < 0) continue;
  446. let [prefix, suffix] = pathname.split("&");
  447. suffix = suffix ?? "";
  448. prefix = prefix.split("/");
  449. let pre = "";
  450. for (let index = 0; index <= prefix.indexOf(item) + 1; index++) {
  451. pre = `${pre}${prefix[index]}/`;
  452. }
  453. return `${pre}${this.loadCount + 2}&${suffix}`;
  454. }
  455. };
  456. let infScroll = new InfiniteScroll(waterfall, {
  457. path,
  458. append: ".item",
  459. outlayer: msnry,
  460. elementScroll: ".scrollBox",
  461. history: false,
  462. historyTitle: false,
  463. hideNav: ".pagination",
  464. status: ".page-load-status",
  465. debug: false,
  466. });
  467. infScroll.on("load", this.modifyItem);
  468. };
  469. modifyItem = (node = doc) => {
  470. if (ck) handleClick(".item a", node);
  471. const items = node.querySelectorAll(".item");
  472. for (const item of items) {
  473. const info = item.querySelector("a .photo-info span:not(.mleft)");
  474. if (info) {
  475. const [titleNode, secondaryNode] = info.childNodes;
  476. const titleTxt = titleNode.textContent.trim();
  477. const title = doc.create("div", { class: "title", title: titleTxt }, titleTxt);
  478. if (lm) title.classList.add("ellipsis");
  479. if (secondaryNode?.nodeName === "BR") {
  480. info.removeChild(secondaryNode);
  481. title.classList.add("line-4");
  482. }
  483. info.replaceChild(title, titleNode);
  484. }
  485.  
  486. let [code, date] = item.querySelectorAll("date");
  487. if (code && date) {
  488. code = code.textContent;
  489. date = date.textContent;
  490. const codeKey = `${code.trim().toUpperCase()}/${date.trim().toUpperCase()}`;
  491. lf.keys().then(keys => {
  492. if (!keys.includes(codeKey)) return;
  493. lf.getItem(codeKey).then(val => {
  494. const resource = val?.resource;
  495. if (!resource?.length) return;
  496. const photo = item.querySelector(".photo-frame");
  497. photo.classList.add("playBtn");
  498. photo.setAttribute("title", "点击播放");
  499. photo.addEventListener("click", e => {
  500. e.stopPropagation();
  501. e.preventDefault();
  502. GM_openInTab(resource[0].link, { active: true });
  503. });
  504. });
  505. });
  506.  
  507. const codes = Array.from(new Set(getCodes(code)));
  508. const reg = new RegExp(`(${codes.join("|")})`, "gi");
  509. rd?.iterate(item => {
  510. const name = item?.name ?? "";
  511. if (name.match(reg)) return item;
  512. }).then(res => {
  513. if (!(res ?? "")) return;
  514. const title = item.querySelector(".title");
  515. title.classList.add("resMatch");
  516. appendBefore(
  517. title,
  518. `<button class="btn btn-xs btn-default" title="列表匹配,点击播放">☆</button>`
  519. );
  520. title.querySelector("button").addEventListener("click", e => {
  521. e.stopPropagation();
  522. e.preventDefault();
  523. GM_openInTab(res.link, { active: true });
  524. });
  525. });
  526. }
  527. }
  528. };
  529. }
  530. class Genre extends Common {
  531. docStart = () => {
  532. GM_addStyle(`
  533. footer { display: none${SUFFIX}; }
  534. button.btn.btn-danger.btn-block.btn-genre {
  535. position: fixed${SUFFIX};
  536. bottom: 0${SUFFIX};
  537. margin: 0${SUFFIX};
  538. left: 0${SUFFIX};
  539. border: 0${SUFFIX};
  540. border-radius: 0${SUFFIX};
  541. }
  542. `);
  543. };
  544. contentLoaded = () => {
  545. if (!doc.querySelector("button.btn.btn-danger.btn-block.btn-genre")) return;
  546. const box = doc.querySelectorAll(".genre-box");
  547. box[box.length - 1].setAttribute("style", "margin-bottom: 65px;");
  548. };
  549. }
  550. class Forum extends Common {
  551. docStart = () => {
  552. GM_addStyle(`
  553. .bcpic,
  554. .banner728,
  555. .sd.sd_allbox > div:last-child { display: none${SUFFIX}; }
  556. .jav-button { margin-top: -3px${SUFFIX}; }
  557. #toptb {
  558. position: fixed${SUFFIX};
  559. top: 0${SUFFIX};
  560. left: 0${SUFFIX};
  561. right: 0${SUFFIX};
  562. z-index: 999${SUFFIX};
  563. }
  564. #wp { margin-top: 55px${SUFFIX}; }
  565. `);
  566. };
  567. }
  568. class Resource extends Common {
  569. docStart = () => {
  570. GM_addStyle(`
  571. .alert.alert-danger.alert-page.error-page, footer { display: none; }
  572. .resItem {
  573. margin: 15px 0;
  574. border-radius: 4px;
  575. overflow: hidden;
  576. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
  577. background-color: ${dm ? SecondaryBackground : "#fff"};
  578. }
  579. .resItem a { color: unset; }
  580. .thumb { aspect-ratio: 100 / 67; }
  581. .thumb img {
  582. width: 100%;
  583. height: 100%;
  584. object-fit: cover;
  585. }
  586. .info { padding: 15px; }
  587. .title { margin-bottom: 6px; }
  588. `);
  589. };
  590. contentLoaded = async () => {
  591. doc.title = "已有资源";
  592. const navbar = doc.querySelector("#navbar .nav.navbar-nav");
  593. navbar.querySelector(".active").classList.remove("active");
  594. navbar.querySelector(".resource").classList.add("active");
  595. const keys = await lf.keys();
  596. let nodes = [];
  597. for (const key of keys) {
  598. const item = await lf.getItem(key);
  599. const resource = item?.resource;
  600. if (!resource?.length) continue;
  601. nodes.push({ ...item, key: key.split("/")[0] });
  602. }
  603. doc.title = `已有资源(${nodes.length})`;
  604. if (!nodes?.length) {
  605. const alert = doc.querySelector(".alert.alert-danger.alert-page.error-page");
  606. alert.querySelector("h4").textContent = "暂无本地缓存数据";
  607. const [zh, en] = alert.querySelectorAll("p");
  608. zh.setAttribute("style", "display: none");
  609. en.textContent = "请检查浏览器相关设置或重新获取数据";
  610. return alert.setAttribute("style", "display: block");
  611. }
  612. nodes.sort((pre, next) => {
  613. let preTime = pre.resource[0].timestamp || Date.parse(pre.resource[0].getDate) / 1000;
  614. let nextTime = next.resource[0].timestamp || Date.parse(next.resource[0].getDate) / 1000;
  615. return preTime > nextTime ? -1 : 1;
  616. });
  617. nodes = nodes.reduce((pre, { thumb, title, resource, href, key, upDate }) => {
  618. return `${pre}
  619. <div class="col-lg-3 col-md-4 col-sm-6">
  620. <div class="resItem">
  621. <a href="${href ?? "/" + key}">
  622. <div class="playBtn thumb" data-link="${resource[0].link}" title="点击播放">
  623. <img src="${thumb}" alt="${title}">
  624. </div>
  625. <div class="info">
  626. <div class="ellipsis title" title="${title}">${title}</div>
  627. <button class="btn btn-xs btn-primary" disabled="disabled">
  628. 更新:${getDate(upDate)}
  629. </button>
  630. <button class="btn btn-xs btn-danger" disabled="disabled">
  631. 资源:${resource.length}
  632. </button>
  633. </div>
  634. </a>
  635. </div>
  636. </div>`;
  637. }, "");
  638. appendAfter(doc.querySelector(".container-fluid .row"), nodes);
  639. for (const item of doc.querySelectorAll(".resItem .thumb")) {
  640. item.addEventListener("click", e => {
  641. e.stopPropagation();
  642. e.preventDefault();
  643. GM_openInTab(item.getAttribute("data-link"), { active: true });
  644. });
  645. }
  646. ck && handleClick(".resItem a");
  647. };
  648. }
  649. class Details extends Common {
  650. code = "";
  651. codeKey = "";
  652. lfItem = {};
  653. config = async (code, date) => {
  654. if (!code || !date) return;
  655. const codeKey = `${code}/${date}`;
  656. this.code = code;
  657. this.codeKey = codeKey;
  658. this.lfItem = (await lf.getItem(codeKey)) ?? {};
  659. };
  660. docStart = () => {
  661. GM_addStyle(`
  662. .info .glyphicon-info-sign,
  663. h4[style="position:relative"],
  664. h4[style="position:relative"] + .row { display: none${SUFFIX}; }
  665. .info ul { margin: 0${SUFFIX}; }
  666. .screencap { max-height: 600px; overflow: hidden; }
  667. #avatar-waterfall, #sample-waterfall, #related-waterfall { margin: -5px${SUFFIX}; }
  668. .photo-info { height: auto${SUFFIX}; }
  669. `);
  670. GM_addStyle(`
  671. #exp { display: none; }
  672. #expBtn {
  673. position: absolute;
  674. top: 0;
  675. height: 40px;
  676. line-height: 40px;
  677. left: 0;
  678. right: 0;
  679. cursor: pointer;
  680. background: rgba(0,0,0,.7);
  681. color: #fff;
  682. margin: 560px 15px 0 15px;
  683. z-index: 99;
  684. }
  685. #expBtn::after { content: "展开"; }
  686. #exp:checked + .screencap { max-height: none; }
  687. #exp:checked + .screencap > #expBtn {
  688. top: auto;
  689. margin: 0 15px;
  690. bottom: 0;
  691. }
  692. #exp:checked + .screencap > #expBtn::after { content: "收起"; }
  693. @media screen and (max-width: 480px) {
  694. #btnGrp { margin-bottom: 15px; }
  695. .screencap { max-height: 280px; }
  696. #expBtn { margin-left: 0; margin-right: 0; }
  697. }
  698. #resBox a { color: #CC0000${SUFFIX}; }
  699. `);
  700. doc.addEventListener("DOMNodeInserted", e => {
  701. const node = e.target;
  702. if (node.nodeName.toLowerCase() !== "tr") return;
  703. const href = node.querySelector("td a")?.href;
  704. if (!href) return;
  705. const td = doc.create("td", { style: "text-align:center;white-space:nowrap" });
  706. const copy = doc.create("a", { href, title: href }, "复制");
  707. const offline = doc.create("a", { href, title: href, style: "margin-left:16px" }, "离线下载");
  708. copy.addEventListener("click", this.copyTxt);
  709. offline.addEventListener("click", this.offLine);
  710. td.appendChild(copy);
  711. td.appendChild(offline);
  712. node.appendChild(td);
  713. });
  714. };
  715. contentLoaded = async () => {
  716. ck && handleClick("a.movie-box");
  717. // copy
  718. const handleCopy = selectors => {
  719. const node = doc.querySelector(selectors);
  720. const txt = node?.textContent?.trim();
  721. if (!node || !txt) return;
  722. const copy = doc.create("a", { title: txt, href: txt, style: "margin-left:16px" }, "复制");
  723. copy.addEventListener("click", this.copyTxt);
  724. node.appendChild(copy);
  725. };
  726. handleCopy("h3");
  727. handleCopy("span[style='color:#CC0000;']");
  728. // expBtn
  729. const screencap = doc.querySelector(".col-md-9.screencap");
  730. insertBefore(screencap, `<input type="checkbox" id="exp">`);
  731. appendAfter(screencap, `<label for="exp" id="expBtn"></label>`);
  732.  
  733. const info = doc.querySelector(".col-md-3.info");
  734. // resource
  735. const res = `<p id="resBox"><span class="header">已有资源:</span><span class="genre">查询中...</span></p>`;
  736. appendAfter(info, res);
  737. // btnGrp
  738. const btnGrp = doc.create("div", { id: "btnGrp", class: "btn-group btn-group-justified" });
  739. const btns = { smartRes: "一键离线", refreshRes: "资源刷新" };
  740. Object.keys(btns).forEach(id => {
  741. const btn = doc.create("a", { id, class: "btn btn-default" }, btns[id]);
  742. btn.addEventListener("click", () => this.handleOpt(id));
  743. btnGrp.appendChild(btn);
  744. });
  745. info.appendChild(btnGrp);
  746. // table
  747. const td = `<td style="text-align:center;white-space:nowrap">操作</td>`;
  748. appendAfter(doc.querySelector("#magnet-table tbody tr"), td);
  749. // photoinfo
  750. for (const item of doc.querySelectorAll(".photo-info span")) item.classList.add("ellipsis");
  751. // config
  752. if (!this.codeKey) {
  753. let [code, date] = doc.querySelectorAll("div.col-md-3.info p");
  754. code = code.querySelectorAll("span")[1].childNodes[0].textContent.trim().toUpperCase();
  755. date = date.childNodes[1].textContent.trim().toUpperCase();
  756. await this.config(code, date);
  757. }
  758.  
  759. this.handleStar();
  760. this.handleResource();
  761. this.handlePreview();
  762. this.handleVideo();
  763. };
  764. load = () => {
  765. const maxHei = 600;
  766. const styleHei = doc.querySelector(".col-md-9.screencap .bigImage img").height;
  767. if (styleHei > maxHei) {
  768. GM_addStyle(`.screencap { max-height: ${maxHei}px; } #expBtn { margin-top: ${maxHei - 40}px; }`);
  769. } else {
  770. GM_addStyle(`.screencap { max-height: ${styleHei + 40}px; } #expBtn { margin-top: ${styleHei}px; }`);
  771. }
  772. };
  773. copyTxt = e => {
  774. e.preventDefault();
  775. e.stopPropagation();
  776. const node = e.target;
  777. const txt = node?.title;
  778. if (!node || !txt) return;
  779. GM_setClipboard(txt);
  780. const text = node?.textContent ?? "";
  781. node.textContent = "成功";
  782. setTimeout(() => {
  783. node.textContent = text;
  784. }, 1000);
  785. };
  786. // 预览大图
  787. handlePreview = async () => {
  788. let image = this.lfItem?.image;
  789. if (!image) {
  790. let res = await request(`https://javstore.net/search/${this.code}.html`);
  791. const href = res?.querySelector("#content_news li a")?.href;
  792. if (!href) return;
  793. res = await request(href);
  794. image = res?.querySelector(".news a img[alt*='.th']")?.src?.replace(".th", "");
  795. if (!image) return;
  796. lf.upItem(this.codeKey, { image });
  797. }
  798. if (!(await request(image))) return;
  799. const img = doc.create("img", { src: image, title: "点击收起", style: "cursor:pointer" });
  800. img.addEventListener("click", () => {
  801. doc.querySelector("#exp").checked = false;
  802. });
  803. img.onload = () => doc.querySelector(".col-md-9.screencap").appendChild(img);
  804. };
  805. // 预览视频
  806. handleVideo = async () => {
  807. let video = this.lfItem?.video;
  808. if (!video) {
  809. const res = await request(`https://www.r18.com/common/search/searchword=${this.code}/`);
  810. video = res?.querySelector("a.js-view-sample")?.getAttribute("data-video-high");
  811. if (!video) return;
  812. lf.upItem(this.codeKey, { video });
  813. }
  814. const title = "预览视频";
  815. const playVideo = e => {
  816. e.preventDefault();
  817. e.stopPropagation();
  818. doc.body.setAttribute("style", "overflow: hidden;");
  819. doc.querySelector("#video-mask").setAttribute("style", "display: flex;");
  820. const video = doc.querySelector("video");
  821. video.play();
  822. video.focus();
  823. doc.onkeydown = event => {
  824. const e = event || window.event;
  825. if (e && e.keyCode === 27) pauseVideo();
  826. };
  827. };
  828. const pauseVideo = () => {
  829. doc.body.setAttribute("style", "overflow: auto;");
  830. doc.querySelector("#video-mask").setAttribute("style", "display: none;");
  831. doc.querySelector("video").pause();
  832. doc.onkeydown = null;
  833. };
  834.  
  835. const videoNode = doc.create("video", { controls: "controls", src: video });
  836. videoNode.currentTime = 5;
  837. videoNode.preload = "auto";
  838. videoNode.muted = true;
  839. const closeBtn = doc.create("button", { title: "Close (Esc)", type: "button", class: "mfp-close" }, "×");
  840. closeBtn.addEventListener("click", pauseVideo);
  841. const mask = doc.create("div", { id: "video-mask", class: "mask" });
  842. mask.appendChild(closeBtn);
  843. mask.appendChild(videoNode);
  844. doc.body.appendChild(mask);
  845.  
  846. const bigImage = doc.querySelector(".bigImage");
  847. const bImg = bigImage.querySelector("img");
  848. const playBtn = doc.create("div", { class: "playBtn", title });
  849. playBtn.addEventListener("click", playVideo);
  850. playBtn.appendChild(bImg);
  851. bigImage.appendChild(playBtn);
  852.  
  853. const thumb = bImg.src;
  854. const box = doc.create("a", { class: "sample-box", href: thumb, title });
  855. box.addEventListener("click", playVideo);
  856. appendAfter(box, `<div class="photo-frame playBtn"><img src="${thumb}"></div>`);
  857.  
  858. let waterfall = doc.querySelector("#sample-waterfall");
  859. if (!waterfall) {
  860. insertBefore(doc.querySelector("div.clearfix"), `<div id="sample-waterfall"></div>`);
  861. waterfall = doc.querySelector("#sample-waterfall");
  862. insertBefore(waterfall, `<h4>樣品圖像</h4>`);
  863. return waterfall.appendChild(box);
  864. }
  865. const ref = waterfall.querySelector("a");
  866. waterfall.insertBefore(box, ref);
  867. const imgBtn = doc.create(
  868. "button",
  869. { title: "樣品圖像", type: "button", class: "mfp-close", style: "right:44px" },
  870. "📷"
  871. );
  872. imgBtn.addEventListener("click", () => {
  873. pauseVideo();
  874. ref.click();
  875. });
  876. mask.appendChild(imgBtn);
  877. };
  878. // 演员列表
  879. handleStar = async () => {
  880. const info = doc.querySelector(".col-md-3.info");
  881. if (!/暫無出演者資訊/g.test(info.textContent)) return;
  882. let star = this.lfItem?.star ?? [];
  883. if (!star?.length) {
  884. const site = "https://javdb.com";
  885. let res = await request(`${site}/search?q=${this.code}`);
  886. const href = res.querySelector("#videos .grid-item a").getAttribute("href");
  887. if (!href) return;
  888. res = await request(`${site}${href}`);
  889. res = res?.querySelectorAll(".panel-block");
  890. if (!res?.length) return;
  891. res = res[res.length - 3]?.querySelector(".value").textContent.trim();
  892. res = res.split(/\n/).filter(item => item.indexOf("♀") !== -1);
  893. star = res.map(item => item.trim().replace("♀", ""));
  894. if (!star?.length) return;
  895. lf.upItem(this.codeKey, { star });
  896. }
  897. const p = doc.create("p");
  898. appendAfter(
  899. p,
  900. star.reduce((acc, cur) => `${acc}<span class="genre"><a href="/search/${cur}">${cur}</a></span>`, "")
  901. );
  902. info.replaceChild(p, doc.querySelector("span.glyphicon.glyphicon-info-sign.mb20")?.nextSibling);
  903. };
  904. // 已有资源(本地)
  905. handleResource = async () => {
  906. const lfItem = await lf.getItem(this.codeKey);
  907.  
  908. let resource = lfItem?.resource ?? [];
  909. const upDate = lfItem?.upDate;
  910. const bool = !upDate || Math.floor((new Date().getTime() - upDate) / 24 / 3600 / 1000) > 3;
  911. if (bool) resource = await this.fetchResource();
  912.  
  913. const resBox = doc.querySelector("#resBox");
  914. for (const old of resBox.querySelectorAll(".genre")) resBox.removeChild(old);
  915. if (!resource?.length) return appendAfter(resBox, `<span class="genre">无</span>`);
  916. const child = resource.reduce((acc, { link, getDate, name }) => {
  917. let thunbName = name.length > 20 ? `${name.substr(0, 20)}...` : name;
  918. return `${acc}<span class="genre"><a href="${link}" title="${getDate}/${name}" target="_blank">${thunbName}</a></span>`;
  919. }, "");
  920. appendAfter(resBox, child);
  921.  
  922. if (lfItem?.thumb && lfItem?.title && lfItem?.href) return;
  923. lf.upItem(this.codeKey, {
  924. thumb: doc.querySelector(".screencap .bigImage img").src,
  925. title: doc.querySelector("h3").childNodes[0].textContent,
  926. href: location.href,
  927. });
  928. };
  929. // 已有资源(115)
  930. fetchResource = async (lname = "") => {
  931. const code = this.code;
  932. let codes = getCodes(code);
  933. if (lname) codes.unshift(encodeURIComponent(lname));
  934. codes = Array.from(new Set(codes));
  935. let { data } = await request("https://webapi.115.com/files/search", "GET", {
  936. search_value: encodeURIComponent(codes.join(" ")),
  937. format: "json",
  938. });
  939. let resource = [];
  940. if (data?.length) {
  941. const reg = new RegExp(`(${codes.join("|")})`, "gi");
  942. data = data.filter(({ n, play_long }) => n.match(reg) && play_long && play_long > 0);
  943. resource = data.map(item => {
  944. return {
  945. fid: item.fid,
  946. cid: item.cid,
  947. link: `https://v.anxia.com/?pickcode=${item.pc}`,
  948. getDate: item.t,
  949. name: item.n,
  950. timestamp: item.te || item.tp,
  951. };
  952. });
  953. }
  954. await lf.upItem(this.codeKey, { upDate: new Date().getTime(), resource });
  955. return resource;
  956. };
  957. // 离线下载
  958. offLine = async e => {
  959. e.preventDefault();
  960. e.stopPropagation();
  961. const node = e.target;
  962. const link = node?.title;
  963. if (!node || !link) return;
  964. GM_setClipboard(link);
  965. const text = node?.textContent ?? "";
  966. node.textContent = "请求中...";
  967. const firstTd = node.parentNode.parentNode.querySelector("td");
  968. const zh = !!firstTd.querySelector("a.btn.btn-mini-new.btn-warning.disabled");
  969. const lname = firstTd.querySelector("a").textContent.trim();
  970. const res = await this.offLineDownload({ link, zh, lname });
  971. node.textContent = text;
  972. notifiy(res);
  973. };
  974. // 排队离线/资源刷新
  975. handleOpt = async action => {
  976. const node = doc.querySelector(`#${action}`);
  977. if (!node) return;
  978. node.classList.toggle("disabled");
  979. const text = node?.textContent ?? "";
  980. node.textContent = "请求中...";
  981.  
  982. if (action === "refreshRes") {
  983. await lf.upItem(this.codeKey, { upDate: 0 });
  984. await this.handleResource();
  985. }
  986. if (action === "smartRes") {
  987. const trs = doc.querySelector("#magnet-table").querySelectorAll("tr");
  988. let magnetArr = [];
  989. for (let index = 1; index < trs.length; index++) {
  990. const item = { link: "", lname: "", zh: false, size: 0, date: 0 };
  991. let [name, size, date] = trs[index].querySelectorAll("td");
  992. if (!name || !size || !date) continue;
  993. item.zh = !!name.querySelector("a.btn.btn-mini-new.btn-warning.disabled");
  994. name = name.querySelector("a");
  995. item.link = name.href;
  996. item.lname = name.textContent.trim();
  997. size = size.querySelector("a").textContent.trim().replace(/gb/gi, "");
  998. if (/mb/gi.test(size)) size = (parseFloat(size) / 1024).toFixed(2);
  999. item.size = Number(size);
  1000. item.date = date.querySelector("a").textContent.trim().replace(/-/g, "");
  1001. magnetArr.push(item);
  1002. }
  1003. magnetArr.sort((pre, next) => {
  1004. if (pre.zh === next.zh) {
  1005. if (pre.size === next.size) return next.date - pre.date;
  1006. return next.size - pre.size;
  1007. } else {
  1008. return pre.zh > next.zh ? -1 : 1;
  1009. }
  1010. });
  1011. for (let index = 0; index < magnetArr.length; index++) {
  1012. const res = await this.offLineDownload(magnetArr[index]);
  1013. if (res?.image !== "info") {
  1014. notifiy(res);
  1015. break;
  1016. }
  1017. if (index !== magnetArr.length - 1) continue;
  1018. notifiy(
  1019. "一键离线失败",
  1020. "远程未查找到新增资源,接口失效或资源被审核",
  1021. "fail",
  1022. "http://115.com/?tab=offline&mode=wangpan"
  1023. );
  1024. }
  1025. }
  1026.  
  1027. node.textContent = text;
  1028. node.classList.toggle("disabled");
  1029. };
  1030. // 请求离线&结果查询
  1031. offLineDownload = async ({ link, zh, lname }) => {
  1032. const fname = `${zh ? "【中文字幕】" : ""}${doc.querySelector("h3").textContent.replace("复制", "")}`;
  1033.  
  1034. let notifiyObj = {
  1035. title: "操作失败,115未登录",
  1036. text: "请登录115账户后再离线下载",
  1037. image: "fail",
  1038. clickUrl: "http://115.com/?mode=login",
  1039. };
  1040. let res = await request("http://115.com/", "GET", {
  1041. ct: "offline",
  1042. ac: "space",
  1043. _: new Date().getTime(),
  1044. });
  1045. if (!res?.sign) return notifiyObj;
  1046. const { sign, time } = res;
  1047. res = await request("http://115.com/web/lixian/?ct=lixian&ac=add_task_url", "POST", {
  1048. url: link.substr(0, 60),
  1049. uid: 0,
  1050. sign,
  1051. time,
  1052. });
  1053. const { state, errcode, error_msg } = res;
  1054. notifiyObj = {
  1055. title: "离线失败",
  1056. text: error_msg,
  1057. image: "info",
  1058. clickUrl: "http://115.com/?tab=offline&mode=wangpan",
  1059. };
  1060. if (`${errcode}` === "911") {
  1061. notifiyObj.title += ",账号异常";
  1062. notifiyObj.text = "验证后使用";
  1063. notifiyObj.image = "fail";
  1064. }
  1065. if (!state) return notifiyObj;
  1066. // 获取旧的本地缓存数据
  1067. let lfItem = (await lf.getItem(this.codeKey)) ?? {};
  1068. if (!lfItem?.resource) lfItem.resource = [];
  1069. // 远程获取搜索结果
  1070. await delay(2000);
  1071. let resource = await this.fetchResource(lname);
  1072. this.handleResource();
  1073. const newRes = resource.filter(item => item.getDate === getDate());
  1074. // 搜索结果资源数至少比本地缓存多且获取日期为当前日期
  1075. if (resource.length <= lfItem.resource.length || newRes.length < 1) {
  1076. notifiyObj.text = "未找到新增资源,接口失效或资源审核";
  1077. return notifiyObj;
  1078. }
  1079. for (let index = 0; index < newRes.length; index++) {
  1080. const { name } = newRes[index];
  1081. const suffix = name.split(".").pop().toLowerCase();
  1082. newRes[index]["rename"] = `${fname}.${suffix}`;
  1083. }
  1084. this.afterAction(newRes.filter(item => item.name.indexOf(fname) === -1));
  1085. return {
  1086. title: "离线成功",
  1087. text: "点击跳转",
  1088. image: "success",
  1089. clickUrl: `https://115.com/?cid=${rootId ?? 0}&offset=0&mode=wangpan`,
  1090. };
  1091. };
  1092. // 离线后重命名,移动,移除原目录等
  1093. afterAction = async items => {
  1094. // rename
  1095. for (const { fid, rename: file_name } of items) {
  1096. await request("http://webapi.115.com/files/edit", "POST", { fid, file_name });
  1097. }
  1098. if (rootId) {
  1099. const params = arr => arr.reduce((acc, cur, idx) => `${acc}&fid[${idx}]=${cur}`, "");
  1100. const fids = Array.from(new Set(items.map(item => item.fid)));
  1101. const cids = Array.from(new Set(items.filter(item => item !== rootId).map(item => item.cid)));
  1102. // move
  1103. const move_proid = `${new Date()}_${~(100 * Math.random())}_0`;
  1104. await request(
  1105. "https://webapi.115.com/files/move",
  1106. "POST",
  1107. `pid=${rootId}&move_proid=${move_proid}${params(fids)}`
  1108. );
  1109. // del
  1110. request("https://webapi.115.com/rb/delete", "POST", `pid=${rootId}&ignore_warn=1${params(cids)}`);
  1111. }
  1112. await lf.upItem(this.codeKey, { upDate: 0 });
  1113. this.handleResource();
  1114. };
  1115. }
  1116.  
  1117. class JavBusScript {
  1118. waterfall = new Waterfall();
  1119. genre = new Genre();
  1120. forum = new Forum();
  1121. resource = new Resource();
  1122. details = new Details();
  1123. }
  1124.  
  1125. const Background = "rgb(18,18,18)";
  1126. const SecondaryBackground = "rgb(32,32,32)";
  1127. const LabelColor = "rgba(255,255,255,.95)";
  1128. const SecondaryLabelColor = "rgb(170,170,170)";
  1129. const Grey = "rgb(49,49,49)";
  1130. const Blue = "rgb(10,132,255)";
  1131. const Orange = "rgb(255,159,10)";
  1132. const Green = "rgb(48,209,88)";
  1133. const Red = "rgb(255,69,58)";
  1134. const darkMode = path => {
  1135. if (path === "forum") return;
  1136. GM_addStyle(`
  1137. ::-webkit-scrollbar {
  1138. width: 8px;
  1139. height: 8px;
  1140. }
  1141. ::-webkit-scrollbar-thumb {
  1142. border-radius: 4px;
  1143. background-color: ${Grey};
  1144. }
  1145. *:not(span) {
  1146. border-color: ${Grey}${SUFFIX};
  1147. outline: ${Grey}${SUFFIX};
  1148. text-shadow: none${SUFFIX};
  1149. }
  1150. body, footer {
  1151. background: ${Background}${SUFFIX};
  1152. color: ${SecondaryLabelColor}${SUFFIX};
  1153. }
  1154. img { filter: brightness(90%)${SUFFIX}; }
  1155. nav { background: ${SecondaryBackground}${SUFFIX}; }
  1156. input { background: ${Background}${SUFFIX}; }
  1157. *::placeholder { color: ${SecondaryLabelColor}${SUFFIX}; }
  1158. button, input, a, h1, h2, h3, h4, h5, h6 {
  1159. color: ${LabelColor}${SUFFIX};
  1160. box-shadow: none${SUFFIX};
  1161. outline: none${SUFFIX};
  1162. }
  1163. .btn.disabled, .btn[disabled], fieldset[disabled] .btn { opacity: .85${SUFFIX}; }
  1164. button, .btn-default, .input-group-addon {
  1165. background: ${Grey}${SUFFIX};
  1166. color: ${LabelColor}${SUFFIX};
  1167. }
  1168. .btn-primary {
  1169. background: ${Blue}${SUFFIX};
  1170. border-color: ${Blue}${SUFFIX};
  1171. }
  1172. .btn-success {
  1173. background: ${Green}${SUFFIX};
  1174. border-color: ${Green}${SUFFIX};
  1175. }
  1176. .btn-danger {
  1177. background: ${Red}${SUFFIX};
  1178. border-color: ${Red}${SUFFIX};
  1179. }
  1180. .btn-warning {
  1181. background: ${Orange}${SUFFIX};
  1182. border-color: ${Orange}${SUFFIX};
  1183. }
  1184. .navbar-nav>.active>a, .navbar-nav>.active>a:focus, .navbar-nav>.active>a:hover, .navbar-nav>.open>a, .dropdown-menu { background: ${Background}${SUFFIX}; }
  1185. .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover { background: ${Grey}${SUFFIX}; }
  1186. .pagination .active a {
  1187. border: none${SUFFIX};
  1188. color: ${LabelColor}${SUFFIX};
  1189. }
  1190. .pagination>li>a, .pagination>li>span {
  1191. background-color: ${Grey}${SUFFIX};
  1192. border: none${SUFFIX};
  1193. color: ${LabelColor}${SUFFIX};
  1194. }
  1195. tr, .modal-content, .alert {
  1196. background: ${SecondaryBackground}${SUFFIX};
  1197. box-shadow: none${SUFFIX};
  1198. }
  1199. tr:hover { background: ${Grey}${SUFFIX}; }
  1200. `);
  1201. if (path === "waterfall") {
  1202. GM_addStyle(`
  1203. .item a { background: ${SecondaryBackground}${SUFFIX}; }
  1204. .photo-info {
  1205. background: ${SecondaryBackground}${SUFFIX};
  1206. color: ${LabelColor}${SUFFIX};
  1207. }
  1208. date { color: ${SecondaryLabelColor}${SUFFIX}; }
  1209. .nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover, .nav-pills>li>a:focus, .nav-pills>li>a:hover { background-color: ${Grey}${SUFFIX}; }
  1210. `);
  1211. }
  1212. if (path === "genre") GM_addStyle(`.genre-box { background-color: ${SecondaryBackground}${SUFFIX}; }`);
  1213. if (path === "forum") {
  1214. GM_addStyle(`
  1215. #toptb,
  1216. .mn > div,
  1217. .biaoqicn_show.slidebar a:not(.on),
  1218. .new4_list_top .dspanonhover,
  1219. div[id*="con_NewOne_"],
  1220. .frame,
  1221. .frame-tab,
  1222. .main-right-p15,
  1223. .bm.bmw.cl,
  1224. .bm.bmw.cl .bm_h.cl { background: ${SecondaryBackground}${SUFFIX}; }
  1225. .jav-footer,
  1226. .nav-title.nav-active a,
  1227. .menu-body-panel.login-detail-wrap,
  1228. .menu-body-panel .icon-arrow-t,
  1229. .new4_list_top,
  1230. .comment-excerpt { background: ${Background}${SUFFIX}; }
  1231. .main-right-zuixin .comment-excerpt:before { border-bottom-color: ${Background}${SUFFIX}; }
  1232. .main-right-tit span { color: ${LabelColor}${SUFFIX}; }
  1233. `);
  1234. }
  1235. if (path === "details") {
  1236. GM_addStyle(`
  1237. .movie, .sample-box, .movie-box, .photo-info { background: ${SecondaryBackground}${SUFFIX}; }
  1238. .photo-info { color: ${LabelColor}${SUFFIX}; }
  1239. .avatar-box, .avatar-box span, .info ul li, .info .star-name {
  1240. background: ${SecondaryBackground}${SUFFIX};
  1241. border-color: ${Grey}${SUFFIX};
  1242. color: ${LabelColor}${SUFFIX};
  1243. }
  1244. `);
  1245. }
  1246. };
  1247.  
  1248. if (/javbus\.com/g.test(host)) {
  1249. const menus = [
  1250. { title: "点击事件", name: "ck", val: ck, key: "c" },
  1251. { title: "暗黑模式", name: "dm", val: dm, key: "d" },
  1252. { title: "滚动加载", name: "lm", val: lm, key: "s" },
  1253. ];
  1254. for (const { title, name, val, key } of menus) {
  1255. GM_registerMenuCommand(
  1256. `${val ? "关闭" : "开启"}${title}`,
  1257. () => {
  1258. GM_setValue(name, !val);
  1259. location.reload();
  1260. },
  1261. key
  1262. );
  1263. }
  1264. GM_registerMenuCommand(
  1265. "离线后操作根目录cid",
  1266. () => {
  1267. const rid = prompt("用于离线后移动,删除等操作", rootId);
  1268. GM_setValue("rid", rid);
  1269. location.reload();
  1270. },
  1271. "a"
  1272. );
  1273.  
  1274. lf.config({ name: "JBDB", storeName: "AVS" });
  1275.  
  1276. if (rootId) {
  1277. rd = lf.createInstance({ name: "JBDB", storeName: "RES" });
  1278. GM_registerMenuCommand(
  1279. "获取网盘列表",
  1280. () => {
  1281. doc.body.setAttribute("style", "overflow: hidden;");
  1282. doc.querySelector("#res-mask").setAttribute("style", "display: flex;");
  1283. },
  1284. "r"
  1285. );
  1286. }
  1287.  
  1288. GM_addStyle(`
  1289. .ad-box { display: none${SUFFIX}; }
  1290. .ellipsis {
  1291. overflow : hidden;
  1292. text-overflow: ellipsis;
  1293. display: -webkit-box;
  1294. -webkit-line-clamp: 1;
  1295. -webkit-box-orient: vertical;
  1296. }
  1297. .line-4 { -webkit-line-clamp: 4; }
  1298. .playBtn { position: relative; }
  1299. .playBtn:after {
  1300. position: absolute;
  1301. background: url(https://javdb.com/packs/media/images/btn-play-b414746c.svg) 50% no-repeat;
  1302. background-color: rgba(0,0,0,.2);
  1303. background-size: 40px;
  1304. content: "";
  1305. top: 0;
  1306. right: 0;
  1307. bottom: 0;
  1308. left: 0;
  1309. transition: all .3s ease-out;
  1310. opacity: .85;
  1311. }
  1312. .playBtn:hover::after { background-color: rgba(0,0,0,0); }
  1313. .playBtn img { filter: none${SUFFIX}; }
  1314. .mask {
  1315. position: fixed;
  1316. width: 100%;
  1317. height: 100%;
  1318. z-index: 9999;
  1319. left: 0;
  1320. top: 0;
  1321. background: rgba(11,11,11,.8);
  1322. justify-content: center;
  1323. align-items: center;
  1324. display: none;
  1325. }
  1326. .resMatch { color: ${Blue}${SUFFIX}; }
  1327. .resMatch button { margin-right: 4px; }
  1328. `);
  1329.  
  1330. const pathReg = {
  1331. waterfall:
  1332. /^\/((uncensored|uncensored\/)?(page\/\d+)?$)|((uncensored\/)?((search|searchstar|actresses|genre|star|studio|label|series|director|member)+\/)|actresses(\/\d+)?)+/i,
  1333. genre: /^\/(uncensored\/)?genre$/i,
  1334. forum: /^\/forum\//i,
  1335. resource: /^\/resource$/i,
  1336. details: /^\/[\w]+(-|_)?[\d]*.*$/i,
  1337. };
  1338. const path = Object.keys(pathReg).filter(key => pathReg[key].test(pathname))[0];
  1339. if (!path) return;
  1340. dm && darkMode(path);
  1341. const jav = new JavBusScript()[path];
  1342. if (!jav) return;
  1343. jav.docStart();
  1344. doc.addEventListener("DOMContentLoaded", () => {
  1345. scriptStart();
  1346. jav.contentLoaded();
  1347. });
  1348. window.addEventListener("load", jav.load);
  1349. }
  1350. if (/captchaapi\.115\.com/g.test(host)) {
  1351. doc.addEventListener("DOMContentLoaded", () => {
  1352. window.focus();
  1353. const btn = doc.querySelector("#js_ver_code_box button[rel='verify']");
  1354. btn.addEventListener("click", () => {
  1355. const interval = setInterval(() => {
  1356. if (doc.querySelector("div[rel='error_box']").getAttribute("style").indexOf("none") !== -1) {
  1357. window.open("", "_self");
  1358. window.close();
  1359. }
  1360. }, 300);
  1361. setTimeout(() => clearInterval(interval), 600);
  1362. });
  1363. });
  1364. }
  1365. })();