Jav小司机

Jav小司机。简单轻量速度快!

  1. // ==UserScript==
  2. // @name Jav小司机
  3. // @namespace wddd
  4. // @version 1.1.5
  5. // @author wddd
  6. // @license MIT
  7. // @include http*://*javlibrary.com/*
  8. // @include http*://*javlib.com/*
  9. // @include http*://*m34z.com/*
  10. // @include http*://*j41g.com/*
  11. // @include http*://*h28o.com/*
  12. // @description Jav小司机。简单轻量速度快!
  13. // @grant GM_xmlhttpRequest
  14. // @grant GM_addStyle
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_log
  18. // @homepage https://github.com/wdwind/JavMiniDriver
  19. // ==/UserScript==
  20.  
  21. // Credit to
  22. // * https://greasyfork.org/zh-CN/scripts/25781
  23. // * https://greasyfork.org/zh-CN/scripts/37122
  24.  
  25. // Change log
  26. // 1.1.5
  27. /**
  28. * Add page selector in video detail page
  29. * Support filters by score and viewers
  30. */
  31. // 1.1.4
  32. /**
  33. * Add support for j41g.com and h28o.com
  34. * Block ad
  35. * Only load the full screenshot until user clicks the thumbnail
  36. */
  37. // 1.1.3
  38. /**
  39. * Issue: https://github.com/wdwind/JavMiniDriver/issues/1#issuecomment-521836751
  40. *
  41. * Update browser history when clicking "load more" button in video list page
  42. * Store the configuration of whether to show the page selector in local storage instead of cookies
  43. * Fix a screenshot bug to handle non-existing images gracefully
  44. * Temporarily remove video from sod.co.jp since it requires a Referer in http request header
  45. * ~~Add a iframe to bypass adult check and DDoS check of sod.co.jp~~
  46. * Other technical refactoring
  47. */
  48. // 1.1.2
  49. /**
  50. * Issue: https://greasyfork.org/zh-CN/forum/discussion/61213/x
  51. *
  52. * Minor updates
  53. * Add javbus torrent search
  54. * Add support for javlib.com and m34z.com
  55. */
  56. // 1.1.1
  57. /**
  58. * Issue: https://github.com/wdwind/JavMiniDriver/issues/1
  59. *
  60. * Change thumbnail font
  61. * Add page selector
  62. * Add japanese-bukkake as backup image screenshot source
  63. * Change image width to max-width when clicking the screenshot to prevent image being over zoomed
  64. * Add more data sources for the screenshots in reviews/comments
  65. */
  66. // 1.1.0
  67. /**
  68. * Simplify code by merging the functions for get more comments/reviews
  69. * Process screenshots in reviews/comments
  70. * Remove redirection
  71. * Get full image url
  72. * Add mouse click effect
  73. */
  74.  
  75. // Utils
  76.  
  77. function setCookie(cookieName, cookieValue, expireDays) {
  78. let expireDate =new Date();
  79. expireDate.setDate(expireDate.getDate() + expireDays);
  80. let expires = "expires=" + ((expireDays == null) ? '' : expireDate.toUTCString());
  81. document.cookie = cookieName + "=" + cookieValue + ";" + expires + ";path=/";
  82. }
  83.  
  84. // Not used
  85. // function getCookie(cookieName) {
  86. // let value = "; " + document.cookie;
  87. // let parts = value.split("; " + cookieName + "=");
  88. // if (parts.length == 2) {
  89. // return parts.pop().split(";").shift();
  90. // }
  91. // }
  92.  
  93. function insertAfter(newNode, referenceNode) {
  94. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  95. }
  96.  
  97. function insertBefore(newNode, referenceNode) {
  98. referenceNode.parentNode.insertBefore(newNode, referenceNode);
  99. }
  100.  
  101. function removeElementIfPresent(element) {
  102. if (element) {
  103. return element.parentNode.removeChild(element);
  104. }
  105. }
  106.  
  107. function parseHTMLText(text) {
  108. try {
  109. let doc = document.implementation.createHTMLDocument('');
  110. doc.documentElement.innerHTML = text;
  111. return doc;
  112. } catch (e) {
  113. console.error('Parse error');
  114. }
  115. }
  116.  
  117. // https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro
  118. function createElementFromHTML(html) {
  119. var template = document.createElement('template');
  120. html = html.trim(); // Never return a text node of whitespace as the result
  121. template.innerHTML = html;
  122. return template.content.firstChild;
  123. }
  124.  
  125. // For the requests in different domains
  126. // GM_xmlhttpRequest is made from the background page, and as a result, it
  127. // doesn't have cookies in the request header
  128. function gmFetch(obj) {
  129. return new Promise((resolve, reject) => {
  130. GM_xmlhttpRequest({
  131. method: obj.method || 'GET',
  132. // timeout in ms
  133. timeout: obj.timeout,
  134. url: obj.url,
  135. headers: obj.headers ? obj.headers : {},
  136. data: obj.data,
  137. onload: (result) => {
  138. if (result.status >= 200 && result.status < 300) {
  139. resolve(result);
  140. } else {
  141. reject(result);
  142. }
  143. },
  144. onerror: reject,
  145. ontimeout: reject,
  146. });
  147. });
  148. }
  149.  
  150. // For the requests in the same domain
  151. // XMLHttpRequest is made within the page, so it will send the cookies
  152. function xhrFetch(obj) {
  153. return fetch(obj.url, {
  154. method: obj.method || 'GET',
  155. headers: obj.headers || {},
  156. body: obj.data,
  157. credentials: 'include',
  158. timeout: obj.timeout,
  159. });
  160. }
  161.  
  162. function xhrFetchWithRetry(obj) {
  163. let fun = (obj) => xhrFetch(obj).then(response => {
  164. if (response.status == 429) {
  165. throw new Error('429 (Too Many Requests)');
  166. } else {
  167. return response;
  168. }
  169. });
  170.  
  171. return retry(fun, obj);
  172. }
  173.  
  174. async function retry(fun, input, max_retries = 10, retries = 0, initial = 8000) {
  175. try {
  176. return await fun(input);
  177. } catch (e) {
  178. if (retries > max_retries) {
  179. throw e;
  180. }
  181.  
  182. GM_log(`Retrying ${retries}, wait ${initial + (2 ** retries) * 1000} ms`);
  183.  
  184. // wait
  185. await new Promise(_ => setTimeout(_, initial + (2 ** retries) * 1000));
  186.  
  187. // retry
  188. return retry(fun, input, max_retries, retries + 1, initial);
  189. }
  190. }
  191.  
  192. // Style
  193.  
  194. function addStyle() {
  195. // social media
  196. GM_addStyle(`
  197. #toplogo {
  198. height: 55px;
  199. }
  200. .socialmedia {
  201. display: none !important;
  202. width: 0% !important;
  203. height: 0% !important;
  204. }
  205. .videothumblist .videos .video {
  206. height: 290px;
  207. }
  208. .thumbnailDetail {
  209. font-size: 14px;
  210. margin-top: 2.5em;
  211. color: #666666;
  212. }
  213. .page_selector {
  214. display: block;
  215. margin-bottom: 15px;
  216. }
  217. .load_more {
  218. text-align: center;
  219. }
  220. #load_next_page {
  221. margin-bottom: 10px;
  222. }
  223. #load_next_page_button {
  224. display: inline;
  225. }
  226. #togglePageSelector {
  227. margin-left: 20px;
  228. font-size: 14px;
  229. vertical-align: text-top;
  230. display: inline;
  231. }
  232. .toggle {
  233. cursor: pointer;
  234. color: blue;
  235. }
  236. .bottombanner2 {
  237. display: none !important;
  238. }
  239. table.displaymode {
  240. table-layout: fixed;
  241. }
  242. td.mid {
  243. text-align: left;
  244. font: bold 12px monospace;
  245. }
  246. input.slider {
  247. direction: rtl;
  248. height: 10px;
  249. margin-left: 10px;
  250. }
  251. .filter {
  252. display: inline-block;
  253. }
  254. .filterMinValue {
  255. display: inline-block;
  256. }
  257. `);
  258.  
  259. // Homepage
  260. if (!window.location.href.includes('.php')) {
  261. GM_addStyle(`
  262. .videothumblist {
  263. height: 645px !important;
  264. }
  265. `);
  266. }
  267. }
  268.  
  269. // Thumbnail
  270. class MiniDriverThumbnail {
  271.  
  272. constructor() {
  273. this.filterKeys = ['minScore', 'minViewer'];
  274. this.filterConfigs = {'minScore': {'value' : GM_getValue('minScore', 0), 'max': 10},
  275. 'minViewer': {'value' : GM_getValue('minViewer', 0), 'max': 100}};
  276. this.videoStats = {};
  277. }
  278.  
  279. execute() {
  280. let videos = document.getElementsByClassName('videos')[0];
  281. document.getElementsByClassName('videothumblist')[0].innerHTML = `<div class="videothumblist">
  282. <div class="videos"></div>
  283. </div>`;
  284. let pageSelector = document.getElementsByClassName('page_selector')[0];
  285. this.updatePageContent(videos, pageSelector);
  286. this.addFilters();
  287. }
  288.  
  289. addFilters() {
  290. let filters = createElementFromHTML(
  291. `<td class="mid">
  292. <div class="filter">
  293. 显示 <label for="score">评分 &gt; </label>
  294. <div class="filterMinValue">0</div>
  295. <!--<input type="number" id="minScore" min="0">-->
  296. <input type="range" id="minScore" min="0" class="slider">
  297. </div>
  298. <div class="filter">
  299. <label for="viewers">观看人数 &gt; </label>
  300. <div class="filterMinValue">0</div>
  301. <!--<input type="number" id="minViewer" min="0">-->
  302. <input type="range" id="minViewer" min="0" class="slider">
  303. </div>
  304. </td>`);
  305. let sliders = filters.getElementsByClassName('slider');
  306. let valueDiv = filters.getElementsByClassName('filterMinValue');
  307.  
  308. function round(num) {
  309. return Math.round(num * 100) / 100;
  310. }
  311. function getSliderValue(input, max) {
  312. return round(100 - input * (100 / max));
  313. }
  314. function getConfigValue(input, max) {
  315. return round(max - input / (100 / max));
  316. }
  317.  
  318. for (let i = 0; i < sliders.length; i++) {
  319. let config = this.filterConfigs[this.filterKeys[i]];
  320.  
  321. sliders[i].value = getSliderValue(config['value'], config['max']);
  322. valueDiv[i].innerText = config['value'];
  323.  
  324. sliders[i].addEventListener('change', () => {
  325. let updatedConfig = getConfigValue(sliders[i].value, config['max']);
  326. valueDiv[i].innerText = updatedConfig;
  327. GM_setValue(this.filterKeys[i], updatedConfig);
  328.  
  329. this.applyFilters();
  330. });
  331. }
  332.  
  333. // Insert filter to the page
  334. let mode = document.getElementsByClassName('displaymode');
  335. if (mode.length > 0) {
  336. let leftMode = mode[0].getElementsByClassName('left');
  337. if (leftMode.length > 0) {
  338. insertAfter(filters, leftMode[0]);
  339. }
  340. }
  341. }
  342.  
  343. applyFilters() {
  344. for (let key in this.videoStats) {
  345. this.applyFilterOn(key);
  346. }
  347. }
  348.  
  349. applyFilterOn(videoKey) {
  350. let video = document.getElementById(videoKey);
  351. if (video) {
  352. let show = true;
  353. for (let filter of this.filterKeys) {
  354. let config = GM_getValue(filter, 0);
  355.  
  356. if (config > 0) {
  357. if (!(filter in this.videoStats[videoKey])
  358. || !this.videoStats[videoKey][filter]
  359. || this.videoStats[videoKey][filter] == NaN
  360. || this.videoStats[videoKey][filter] < config) {
  361. show = false;
  362. if (!show) {
  363. break;
  364. }
  365. }
  366. }
  367. }
  368.  
  369. video.style.display = show ? 'inline-block' : 'none';
  370. }
  371. }
  372.  
  373. updatePageContent(videos, pageSelector) {
  374. // Add videos to the page
  375. let currentVideos = document.getElementsByClassName('videos')[0];
  376. if (videos) {
  377. Array.from(videos.children).forEach(video => {
  378. currentVideos.appendChild(video);
  379. this.updateVideoDetail(video);
  380. this.updateVideoEvents(video);
  381. });
  382. }
  383.  
  384. // Replace page selector content
  385. let pageSelectorId = 'pageSelectorThumbnail';
  386. let showPageSelector = GM_getValue(pageSelectorId, 'none') != 'block' ? 'none' : 'block';
  387. document.getElementsByClassName('page_selector')[0].innerHTML = pageSelector.innerHTML;
  388. document.getElementsByClassName('page_selector')[0].id = pageSelectorId;
  389. document.getElementsByClassName('page_selector')[0].style.display = showPageSelector;
  390. }
  391.  
  392. async updateVideoDetail(video) {
  393. if (video.id.includes('vid_')) {
  394. let request = {url: `/cn/?v=${video.id.substring(4)}`};
  395. let response = await xhrFetchWithRetry(request).catch(err => {GM_log(err); return;});
  396. let responseText = await response.text().catch(err => {GM_log(err); return;});
  397. let videoDetailsDoc = parseHTMLText(responseText);
  398.  
  399. // Video date
  400. let videoDate = '';
  401. if (videoDetailsDoc.getElementById('video_date')) {
  402. videoDate = videoDetailsDoc.getElementById('video_date').getElementsByClassName('text')[0].innerText;
  403. }
  404.  
  405. // Video score
  406. let videoScore = '';
  407. if (videoDetailsDoc.getElementById('video_review')) {
  408. let videoScoreStr = videoDetailsDoc.getElementById('video_review').getElementsByClassName('score')[0].innerText;
  409. videoScore = videoScoreStr.substring(1, videoScoreStr.length - 1);
  410. if (!(video.id in this.videoStats)) {
  411. this.videoStats[video.id] = {};
  412. }
  413. this.videoStats[video.id]['minScore'] = parseFloat(videoScore);
  414. }
  415.  
  416. // Video watched
  417. let videoWatched = '0';
  418. if (videoDetailsDoc.getElementById('watched')) {
  419. videoWatched = videoDetailsDoc.getElementById('watched').getElementsByTagName('a')[0].innerText;
  420. if (!(video.id in this.videoStats)) {
  421. this.videoStats[video.id] = {};
  422. }
  423. this.videoStats[video.id]['minViewer'] = parseFloat(videoWatched);
  424. }
  425.  
  426. let videoDetailsHtml = `
  427. <div class="thumbnailDetail">
  428. <span>${videoDate}</span>&nbsp;&nbsp;<span style='color:red;'>${videoScore}</span>
  429. <br/>
  430. <span>${videoWatched} 人看过</span>
  431. </div>
  432. `;
  433. let videoDetails = createElementFromHTML(videoDetailsHtml);
  434. video.insertBefore(videoDetails, video.getElementsByClassName('toolbar')[0]);
  435.  
  436. // Apply filter
  437. this.applyFilterOn(video.id);
  438. }
  439. }
  440.  
  441. updateVideoEvents(video) {
  442. if (video) {
  443. // Prevent existing listeners https://stackoverflow.com/a/46986927/4214478
  444. video.addEventListener('mouseout', (event) => {
  445. event.stopImmediatePropagation();
  446. video.getElementsByClassName('toolbar')[0].style.display = 'none';
  447. }, true);
  448. video.addEventListener('mouseover', (event) => {
  449. event.stopImmediatePropagation();
  450. video.getElementsByClassName('toolbar')[0].style.display = 'block';
  451. }, true);
  452. }
  453. }
  454.  
  455. async getNextPage(url) {
  456. // Update page URL and history
  457. history.pushState(history.state, window.document.title, url);
  458.  
  459. // Fetch next page
  460. let response = await xhrFetchWithRetry({url: url}).catch(err => {GM_log(err); return;});
  461. let responseText = await response.text().catch(err => {GM_log(err); return;});
  462. let nextPageDoc = parseHTMLText(responseText);
  463.  
  464. // Update page content
  465. let videos = nextPageDoc.getElementsByClassName('videos')[0];
  466. let pageSelector = nextPageDoc.getElementsByClassName('page_selector')[0];
  467. this.updatePageContent(videos, pageSelector);
  468. }
  469. }
  470.  
  471. class MiniDriver {
  472. execute() {
  473. let javUrl = new URL(window.location.href);
  474. this.javVideoId = javUrl.searchParams.get('v');
  475.  
  476. // Video page
  477. if (this.javVideoId != null) {
  478. this.addStyle();
  479. this.setEditionNumber();
  480. this.updateTitle();
  481. this.addScreenshot();
  482. this.addTorrentLinks();
  483. this.updateReviews();
  484. this.updateComments();
  485. this.getPreview();
  486. }
  487. }
  488.  
  489. addStyle() {
  490. // left menu
  491. GM_addStyle(`
  492. #leftmenu {
  493. display: none;
  494. width: 0%;
  495. }
  496. #rightcolumn {
  497. margin-left: 10px;
  498. }
  499. /*
  500. #video_title .post-title:hover {
  501. text-decoration: underline;
  502. text-decoration-color: #CCCCCC;
  503. }
  504. */
  505. #video_id .text {
  506. color: red;
  507. }
  508. #torrents > table {
  509. width:100%;
  510. text-align: center;
  511. border: 2px solid grey;
  512. }
  513. #torrents tr td + td {
  514. border-left: 2px solid grey;
  515. }
  516. #video_favorite_edit {
  517. margin-bottom: 20px;
  518. }
  519. #torrents {
  520. margin-bottom: 20px;
  521. }
  522. #preview {
  523. margin-bottom: 20px;
  524. }
  525. #preview video {
  526. max-width: 100%;
  527. max-height: 80vh;
  528. }
  529. .screenshot {
  530. cursor: pointer;
  531. max-width: 25%;
  532. display: block;
  533. }
  534. .clickToCopy {
  535. cursor: pointer;
  536. }
  537. `);
  538. }
  539.  
  540. setEditionNumber() {
  541. let edition = document.getElementById('video_id').getElementsByClassName('text')[0];
  542. this.editionNumber = edition.innerText;
  543. }
  544.  
  545. async updateTitle() {
  546. let videoTitle = document.getElementById('video_title');
  547. let postTitle = videoTitle.getElementsByClassName('post-title')[0];
  548. postTitle.innerText = postTitle.getElementsByTagName('a')[0].innerText;
  549.  
  550. // Add English title
  551. if (!window.location.href.includes('/en/')) {
  552. let request = {url: `/en/?v=${this.javVideoId}`};
  553. let response = await xhrFetchWithRetry(request).catch(err => {GM_log(err); return;});
  554. let responseText = await response.text().catch(err => {GM_log(err); return;});
  555. let videoDetailsDoc = parseHTMLText(responseText);
  556. let englishTitle = videoDetailsDoc.getElementById('video_title')
  557. .getElementsByClassName('post-title')[0]
  558. .getElementsByTagName('a')[0].innerText;
  559. postTitle.innerHTML = `${postTitle.innerText}<br/>${englishTitle}`;
  560. }
  561. }
  562.  
  563. scrollToTop(element) {
  564. let distanceToTop = element.getBoundingClientRect().top;
  565. if (distanceToTop < 0) {
  566.  
  567. window.scrollBy(0, distanceToTop);
  568. }
  569. }
  570.  
  571. screenShotOnclick(element) {
  572. if (element.style['max-width'] != '100%') {
  573. element.style['max-width'] = '100%';
  574. } else {
  575. element.style['max-width'] = '25%';
  576. }
  577. this.scrollToTop(element);
  578. }
  579.  
  580. lazyScreenShotOnclick(element) {
  581. let currentSrc = element.src;
  582. element.src = element.dataset.src;
  583. element.dataset.src = currentSrc;
  584. element.style['max-width'] = '100%';
  585. this.scrollToTop(element);
  586. }
  587.  
  588. async addScreenshot() {
  589. let javscreensUrl = `http://javscreens.com/images/${this.editionNumber}.jpg`;
  590. let videoDates = document.getElementById('video_date').getElementsByClassName('text')[0].innerText.split('-');
  591. let jbUrl = `http://img.japanese-bukkake.net/${videoDates[0]}/${videoDates[1]}/${this.editionNumber}_s.jpg`;
  592. for (let url of [javscreensUrl, jbUrl]) {
  593. let img = await this.loadImg(url).catch((img) => {return img;});
  594. if (img && img.naturalHeight > 200) {
  595. insertBefore(img, document.getElementById('rightcolumn').getElementsByClassName('socialmedia')[0]);
  596. img.addEventListener('click', () => this.screenShotOnclick(img));
  597. // Valid screenshot loaded, break the loop
  598. break;
  599. }
  600. removeElementIfPresent(img);
  601. }
  602. }
  603.  
  604. loadImg(url) {
  605. return new Promise(function (resolve, reject) {
  606. GM_xmlhttpRequest({
  607. method: 'GET',
  608. responseType: 'blob',
  609. url: url,
  610. onload: (result) => {
  611. if (result.status >= 200 && result.status < 300) {
  612. let img = createElementFromHTML(`<img class="screenshot" title="">`);
  613. insertBefore(img, document.getElementById('rightcolumn').getElementsByClassName('socialmedia')[0]);
  614. img.src = window.URL.createObjectURL(result.response);
  615. img.onload = () => resolve(img);
  616. img.onerror = () => reject(img);
  617. } else {
  618. reject();
  619. }
  620. },
  621. onerror: reject,
  622. ontimeout: reject,
  623. });
  624. });
  625. }
  626.  
  627. addTorrentLinks() {
  628. let sukebei = `https://sukebei.nyaa.si/?f=0&c=0_0&q=${this.editionNumber}`;
  629. let btsow = `https://btos.pw/search/${this.editionNumber}`;
  630. let javbus = `https://www.javbus.com/${this.editionNumber}`;
  631. let torrentKitty = `https://www.torrentkitty.tv/search/${this.editionNumber}`;
  632. let tokyotosho = `https://www.tokyotosho.info/search.php?terms=${this.editionNumber}`;
  633. let biedian = `https://biedian.me/search?source=%E7%A7%8D%E5%AD%90%E6%90%9C&s=time&p=1&k=${this.editionNumber}`;
  634. let btDigg = `http://btdig.com/search?q=${this.editionNumber}`;
  635. let idope = `https://idope.se/torrent-list/${this.editionNumber}/`;
  636.  
  637. let torrentsHTML = `
  638. <div id="torrents">
  639. <!--
  640. <form id="form-btkitty" method="post" target="_blank" action="http://btkittyba.co/">
  641. <input type="hidden" name="keyword" value="${this.editionNumber}">
  642. <input type="hidden" name="hidden" value="true">
  643. </form>
  644. <form id="form-btdiggs" method="post" target="_blank" action="http://btdiggba.me/">
  645. <input type="hidden" name="keyword" value="${this.editionNumber}">
  646. </form>
  647. -->
  648. <table>
  649. <tr>
  650. <td><strong>种子:</strong></td>
  651. <td><a href="${sukebei}" target="_blank">sukebei</a></td>
  652. <td><a href="${btsow}" target="_blank">btsow</a></td>
  653. <td><a href="${javbus}" target="_blank">javbus</a></td>
  654. <td><a href="${torrentKitty}" target="_blank">torrentKitty</a></td>
  655. <td><a href="${tokyotosho}" target="_blank">tokyotosho</a></td>
  656. <td><a href="${biedian}" target="_blank">biedian</a></td>
  657. <td><a href="${btDigg}" target="_blank">btDigg</a></td>
  658. <td><a href="${idope}" target="_blank">idope</a></td>
  659. <!--
  660. <td><a id="btkitty" href="JavaScript:Void(0);" onclick="document.getElementById('form-btkitty').submit();">btkitty</a></td>
  661. <td><a id="btdiggs" href="JavaScript:Void(0);" onclick="document.getElementById('form-btdiggs').submit();">btdiggs</a></td>
  662. -->
  663.  
  664. </tr>
  665. </table>
  666. </div>
  667. `;
  668.  
  669. let torrents = createElementFromHTML(torrentsHTML);
  670. insertAfter(torrents, document.getElementById('video_favorite_edit'));
  671. }
  672.  
  673. updateReviews() {
  674. // Remove existing reviews
  675. let videoReviews = document.getElementById('video_reviews');
  676. Array.from(videoReviews.children).forEach(child => {
  677. if (child.id.includes('review')) {
  678. child.parentNode.removeChild(child);
  679. }
  680. });
  681.  
  682. // Add all reviews
  683. this.getNextPage(1, 'reviews');
  684. }
  685.  
  686. async getNextPage(page, pageType) {
  687. let pageSelectorId = 'page_selector_' + pageType;
  688. let urlPath = 'video' + pageType;
  689. let elementsId = 'video_' + pageType;
  690.  
  691. // Load more reviews
  692. let request = {url: `/cn/${urlPath}.php?v=${this.javVideoId}&mode=2&page=${page}`};
  693. let response = await xhrFetchWithRetry(request).catch(err => {GM_log(err); return;});
  694. let responseText = await response.text().catch(err => {GM_log(err); return;});
  695. let doc = parseHTMLText(responseText);
  696.  
  697. // Remove the page selector div in current page
  698. let oldPageSelectorDiv = document.getElementById(pageSelectorId);
  699. if (oldPageSelectorDiv != null) {
  700. oldPageSelectorDiv.parentNode.removeChild(oldPageSelectorDiv);
  701. }
  702.  
  703. // Get comments/reviews in the next page
  704. let elements = doc.getElementById(elementsId);
  705. if (!elements.getElementsByClassName('t')[0] || !doc.getElementsByClassName('page_selector')[0]) {
  706. return;
  707. }
  708.  
  709. // Set element texts
  710. Array.from(elements.getElementsByClassName('t')).forEach(element => {
  711. let elementText = parseBBCode(escapeHtml(element.getElementsByTagName('textarea')[0].innerText));
  712. let elementHtml = createElementFromHTML(`<div>${parseHTMLText(elementText).body.innerHTML}</div>`);
  713. element.getElementsByClassName('text')[0].replaceWith(this.processUrls(elementHtml));
  714. });
  715.  
  716. // Append elements to the page
  717. let currentElements = document.getElementById(elementsId);
  718. let bottomLine = currentElements.getElementsByClassName('grey')[0];
  719. Array.from(elements.children).forEach(element => {
  720. if (element.tagName == 'TABLE' || element.tagName == 'TD') {
  721. currentElements.insertBefore(element, bottomLine);
  722. }
  723. });
  724.  
  725. // Append page selector
  726. let showPageSelector = GM_getValue(pageSelectorId, 'none') != 'block' ? 'none' : 'block';
  727. let pageSelector = doc.getElementsByClassName('page_selector')[0];
  728. if (pageSelector) {
  729. pageSelector.style.display = showPageSelector;
  730. pageSelector.id = pageSelectorId;
  731. let as = pageSelector.getElementsByTagName('a');
  732. for (let a of as) {
  733. let nextPage = (new URL(a.href, a.href.includes('https') ? undefined : 'https://www.javlibrary.com/')).searchParams.get('page');
  734. a.removeAttribute('href');
  735. a.style.cursor = 'pointer';
  736. a.addEventListener('click', async () => this.getNextPage(nextPage ? parseInt(nextPage) : 1, pageType));
  737. }
  738. insertAfter(pageSelector, currentElements);
  739. }
  740. }
  741.  
  742. updateComments() {
  743. // Remove existing comments
  744. let videoComments = document.getElementById('video_comments');
  745. Array.from(videoComments.children).forEach(child => {
  746. if (child.id.includes('comment')) {
  747. child.parentNode.removeChild(child);
  748. }
  749. });
  750.  
  751. // Add all comments
  752. this.getNextPage(1, 'comments');
  753. }
  754.  
  755. processUrls(content) {
  756. Array.from(content.getElementsByTagName('a')).forEach(a => {
  757. if (a.href.includes('redirect.php?url=')) {
  758. let encodedRealUrl = a.href.replace('redirect.php?url=', '');
  759. let realUrl = decodeURIComponent(encodedRealUrl);
  760. if (realUrl.indexOf('&ver=') > 0) {
  761. realUrl = realUrl.substring(0, realUrl.indexOf('&ver='));
  762. }
  763. a.href = realUrl;
  764. }
  765. });
  766. return content;
  767. }
  768.  
  769. getPreview() {
  770. let nativeDmm = async() => {
  771. let dmmCid = document.getElementsByClassName('btn_videoplayer')[0].getAttribute('attr-data');
  772. // let request = {url: `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${dmmCid}/mtype=AhRVShI_/service=litevideo/mode=/width=560/height=360/`};
  773. let request = {url: `https://www.dmm.co.jp/service/-/html5_player/=/cid=${dmmCid}/mtype=AhRVShI_/service=mono/floor=dvd/mode=/`}
  774.  
  775. let result = await gmFetch(request).catch(err => {GM_log(err); return;});
  776. let doc = parseHTMLText(result.responseText);
  777.  
  778. // Very hacky... Didn't find a way to parse the HTML with JS.
  779. for (let script of doc.getElementsByTagName('script')) {
  780. if (script.innerText != null && script.innerText.includes('.mp4')) {
  781. for (let line of script.innerText.split('\n')) {
  782. if (line.includes('.mp4')) {
  783. line = line.substring(line.indexOf('{'), line.lastIndexOf(';'));
  784. let videoSrc = JSON.parse(line).src;
  785. if (!videoSrc.startsWith('http')) {
  786. videoSrc = 'http:' + videoSrc;
  787. }
  788. return videoSrc;
  789. }
  790. }
  791. }
  792. }
  793. }
  794.  
  795. // r18 site is shut down
  796. // let r18 = async () => {
  797. // let request = {url: `https://www.r18.com/common/search/order=match/searchword=${this.editionNumber}/`};
  798. // let result = await gmFetch(request).catch(err => {GM_log(err); return;});
  799. // let videoTag = parseHTMLText(result.responseText).querySelector('.js-view-sample');
  800. // let src = ['high', 'med', 'low']
  801. // .map(i => videoTag.getAttribute('data-video-' + i))
  802. // .find(i => i);
  803. // return src;
  804. // }
  805.  
  806. let javTrailer = async () => {
  807. let searchRequest = {
  808. url: `https://javtrailers.com/api/autocomplete?query=${this.editionNumber}&searchtype=id&lang=en`,
  809. headers: {
  810. authorization: 'AELAbPQCh_fifd93wMvf_kxMD_fqkUAVf@BVgb2!md@TNW8bUEopFExyGCoKRcZX',
  811. // cookie: 'auth.strategy=local; user-country=US; searchterm=fset-411; searchtype=id',
  812. // referer: 'https://javtrailers.com/',
  813. }
  814. };
  815. let searchResult = await gmFetch(searchRequest).catch(err => {GM_log(err); return;});
  816.  
  817. let results = JSON.parse(searchResult.responseText).results;
  818. for (let result of results) {
  819. if (this.editionNumber === result.dvdId) {
  820. let videoRequest = {
  821. url : `https://javtrailers.com/api/video/${result.contentId}`,
  822. headers: {
  823. authorization: 'AELAbPQCh_fifd93wMvf_kxMD_fqkUAVf@BVgb2!md@TNW8bUEopFExyGCoKRcZX',
  824. // cookie: 'auth.strategy=local; user-country=US; searchterm=fset-411; searchtype=id',
  825. // referer: 'https://javtrailers.com/video/1fset00411',
  826. }
  827. };
  828. let videoResult = await gmFetch(videoRequest).catch(err => {GM_log(err); return;});
  829. let trailerUrl = JSON.parse(videoResult.responseText).video.trailer;
  830. if (trailerUrl.includes('.m3u8')) {
  831. GM_log(trailerUrl);
  832. GM_log('.m3u8 is not supported by HTML video tag on some browsers.');
  833. return;
  834. } else {
  835. return trailerUrl;
  836. }
  837. }
  838. }
  839. }
  840.  
  841. let dmm = async () => {
  842. let dmmCid = await this.getDmmCid();
  843.  
  844. if (dmmCid == null || dmmCid == '') {
  845. return;
  846. }
  847.  
  848. // let request = {url: `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${dmmCid}/mtype=AhRVShI_/service=litevideo/mode=/width=560/height=360/`};
  849. let request = {url: `https://www.dmm.co.jp/service/-/html5_player/=/cid=${dmmCid}/mtype=AhRVShI_/service=mono/floor=dvd/mode=/`}
  850.  
  851. let result = await gmFetch(request).catch(err => {GM_log(err); return;});
  852. let doc = parseHTMLText(result.responseText);
  853.  
  854. // Very hacky... Didn't find a way to parse the HTML with JS.
  855. for (let script of doc.getElementsByTagName('script')) {
  856. if (script.innerText != null && script.innerText.includes('.mp4')) {
  857. for (let line of script.innerText.split('\n')) {
  858. if (line.includes('.mp4')) {
  859. line = line.substring(line.indexOf('{'), line.lastIndexOf(';'));
  860. let videoSrc = JSON.parse(line).src;
  861. if (!videoSrc.startsWith('http')) {
  862. videoSrc = 'http:' + videoSrc;
  863. }
  864. return videoSrc;
  865. }
  866. }
  867. }
  868. }
  869. }
  870.  
  871. // let sod = async () => {
  872. // let request = {url: `https://ec.sod.co.jp/prime/videos/sample.php?id=${this.editionNumber}`};
  873. // let result = await gmFetch(request).catch(err => {GM_log(err); return;});
  874. // let doc = parseHTMLText(result.responseText);
  875. // return doc.getElementsByTagName('source')[0].src;
  876. // }
  877.  
  878. // Site closed?
  879. let jav321 = async () => {
  880. let request = {
  881. url: `https://www.jav321.com/search`,
  882. method: 'POST',
  883. data: `sn=${this.editionNumber}`,
  884. headers: {
  885. referer: 'https://www.jav321.com/',
  886. 'content-type': 'application/x-www-form-urlencoded',
  887. },
  888. };
  889.  
  890. let result = await gmFetch(request).catch(err => {GM_log(err); return;});
  891. let doc = parseHTMLText(result.responseText);
  892. return doc.getElementsByTagName('source')[0].src;
  893. }
  894.  
  895. let kv = async () => {
  896. if (this.editionNumber.includes('KV-')) {
  897. return `http://fskvsample.knights-visual.com/samplemov/${this.editionNumber.toLowerCase()}-samp-st.mp4`;
  898. }
  899.  
  900. return;
  901. }
  902. // // Prepare for sod adult check and DDoS check
  903. // // iframe vs. embed vs. object https://stackoverflow.com/a/21115112/4214478
  904. // // ifrmae sandbox https://www.w3schools.com/tags/att_iframe_sandbox.asp
  905. // insertBefore(
  906. // createElementFromHTML(`<iframe src="https://ec.sod.co.jp/prime/_ontime.php"
  907. // style="display:none;" referrerpolicy="no-referrer" sandbox>
  908. // </iframe>`),
  909. // document.getElementById('topmenu'));
  910.  
  911. let previewSearchSources = document.getElementsByClassName('btn_videoplayer').length > 0 ? [nativeDmm] : [javTrailer, dmm, jav321, kv];
  912. Promise.all(
  913. previewSearchSources.map(source => source().catch(err => {GM_log(err); return;}))
  914. ).then(responses => {
  915. GM_log(responses);
  916.  
  917. let videoHtml = responses
  918. .filter(response => response != null
  919. && this.includesEditionNumber(response)
  920. && !response.includes('//_sample.mp4'))
  921. .map(response => `<source src="${response}">`)
  922. .join('');
  923. if (videoHtml != '') {
  924. let previewHtml = `
  925. <div id="preview">
  926. <video controls onloadstart="this.volume=0.5">
  927. <meta name="referrer" content="no-referrer">
  928. ${videoHtml}
  929. </video>
  930. </div>
  931. `;
  932. insertAfter(createElementFromHTML(previewHtml), document.getElementById('torrents'));
  933. }
  934. });
  935. }
  936.  
  937. includesEditionNumber(str) {
  938. return str != null
  939. // && str.includes(this.editionNumber.toLowerCase().split('-')[0])
  940. && str.includes(this.editionNumber.toLowerCase().split('-')[1]);
  941. }
  942.  
  943. async getDmmCid() {
  944. let getCidFromUrl = (url) => {
  945. if (url.includes('dmm.co.jp') && this.includesEditionNumber(url)) {
  946. let cid = url.split('/').at(-2);
  947. return cid;
  948. }
  949. }
  950.  
  951. let profileImageUrl = document.getElementById('video_jacket_img').src;
  952. let cid = getCidFromUrl(profileImageUrl);
  953. if (cid !== null) {
  954. return cid;
  955. }
  956. let urlPattern = /(http|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?/g;
  957. for (let url of document.body.innerHTML.match(urlPattern)) {
  958. cid = getCidFromUrl(url);
  959. if (cid != null) {
  960. return cid;
  961. }
  962. }
  963.  
  964. let getCidFromSearchEngine = async (searchUrl) => {
  965. let request = {url: searchUrl};
  966. let response = await gmFetch(request).catch(err => {GM_log(err); return;});
  967. let pattern = /(cid=[\w]+|pid=[\w]+)/g;
  968. for (let match of response.responseText.match(pattern)) {
  969. if (this.includesEditionNumber(match)) {
  970. return match.replace(/(cid=|pid=)/, '');
  971. }
  972. }
  973. }
  974.  
  975. // Find dmm cid from search engines
  976. let bingCid = getCidFromSearchEngine(`https://www.bing.com/search?q=${this.editionNumber.toLowerCase()}+site%3awww.dmm.co.jp`);
  977. if (bingCid != null) {
  978. return bingCid;
  979. }
  980.  
  981. let googleCid = await getCidFromSearchEngine(`https://www.google.com/search?q=${this.editionNumber}+site%3Awww.dmm.co.jp`);
  982. if (googleCid != null) {
  983. return googleCid;
  984. }
  985. }
  986. }
  987.  
  988. // Need `// @run-at document-start` to override the default addEventListener
  989. // Check https://stackoverflow.com/a/26269087/4214478 and https://stackoverflow.com/a/57437878/4214478
  990. // EventTarget.prototype.addEventListenerBase = EventTarget.prototype.addEventListener;
  991. // EventTarget.prototype.addEventListener = function(type, listener) {
  992. // if (this == document && type == 'click') {
  993. // GM_log('Prevent adding click event on "document" element. Event listener: ' + listener.toString());
  994. // return;
  995. // }
  996. // this.addEventListenerBase(type, listener);
  997. // };
  998.  
  999. function blockAds() {
  1000. // Not open ad url
  1001. // https://stackoverflow.com/a/9172526
  1002. // https://stackoverflow.com/a/4658196
  1003.  
  1004. let adSites = ['yuanmengbi', 'zhaijv', 'henanlvyi'];
  1005.  
  1006. let scope = (typeof unsafeWindow === "undefined") ? window : unsafeWindow;
  1007. scope.open = function(open) {
  1008. return function(url, name, features) {
  1009. if (adSites.some(site => url.includes(site))) {
  1010. return;
  1011. }
  1012. return open.call(scope, url, name, features);
  1013. };
  1014. }(scope.open);
  1015. }
  1016.  
  1017. // Block ad
  1018. blockAds();
  1019.  
  1020. // Adult check
  1021. setCookie('over18', 18);
  1022.  
  1023. // Style change
  1024. addStyle();
  1025. if (!window.location.href.includes('.php')
  1026. && (window.location.href.includes('?v=') || window.location.href.includes('&v='))) {
  1027. new MiniDriver().execute();
  1028. } else {
  1029. new MiniDriverThumbnail().execute();
  1030. }