JavBus工具

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

Stan na 12-11-2021. Zobacz najnowsza wersja.

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