JAV Manager

一键整合和过滤多Jav网站的观影记录与偏好,屏蔽你不想看的,高亮你喜欢的演员,让你在最短时间内找到真正想看的内容

  1. // ==UserScript==
  2. // @name JAV Manager
  3. // @namespace https://example.com
  4. // @version 1.0
  5. // @description 一键整合和过滤多Jav网站的观影记录与偏好,屏蔽你不想看的,高亮你喜欢的演员,让你在最短时间内找到真正想看的内容
  6. // @match https://jinjier.art/sql*
  7. // @match https://javdb.com/*
  8. // @exclude https://javdb.com/actors/*
  9. // @exclude https://javdb.com/v/*
  10. // @exclude https://javdb.com/search?q=*
  11. // @match https://www.javlibrary.com/*/vl_bestrated.php*
  12. // @match https://www.javlibrary.com/*/vl_mostwanted.php*
  13. // @match https://www.javlibrary.com/*/vl_update.php*
  14. // @match https://www.javlibrary.com/*/vl_genre.php*
  15. // @match https://www.javlibrary.com/*/vl_newrelease.php*
  16. // @match https://www.javlibrary.com/*/vl_newentries.php*
  17. // @match https://www.javlibrary.com/*/
  18. // @grant GM_xmlhttpRequest
  19. // @grant GM_download
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @connect javbus.com
  23. // @connect jable.tv
  24. // @connect javmenu.com
  25. // @connect javdb.com
  26. // @license MIT
  27. // ==/UserScript==
  28.  
  29. (function () {
  30. 'use strict';
  31.  
  32. /*******************************
  33. * Block 1
  34. * 1. Constant definitions
  35. * 2. Utility functions
  36. * 3. Style injection
  37. * 4. Create section function
  38. * 5. Keyword management panel (UI skeleton)
  39. *******************************/
  40.  
  41. // ===== 1. Constant definitions =====
  42. const STORAGE_KEY = 'sql_filter_keywords'; // 屏蔽
  43. const FAVORITES_KEY = 'sql_favorite_keywords'; // 喜爱
  44. const KNOWN_KEY = 'sql_known_keywords'; // 认识
  45. const WATCHED_KEY = 'sql_watched_keywords'; // 看过
  46. const SQL_STORAGE_KEY = 'sql_last_executed'; // 上次执行的SQL
  47. const FILTER_TOGGLE_KEY = 'sql_filter_enabled'; // 屏蔽功能开关
  48. const SORT_PRIORITY_KEY = 'sql_sort_priority_enabled'; // 按照priority排序开关
  49. const SORT_ACTOR_KEY = 'sql_sort_actor_enabled'; // 按照演员信息排序开关
  50. const NUMBER_ACTOR_STORAGE_KEY = 'sql_number_actor_info'; // 番号与演员信息存储键名
  51. const ACTORS_COLUMN_SHOULD_INSERT = 'actors_column_should_insert';
  52. const ACTORS_COLUMN_INSERTED_KEY = 'actors_column_inserted'; // 存储演员列是否已插入
  53.  
  54. let isSelectingText = false; // Mark if the user is currently selecting text
  55.  
  56. // ===== 2. Utility functions =====
  57. // Remove brackets and their contents from actress name
  58. function stripParentheses(text) {
  59. return text.replace(/\s*([^)]*)/g, '').replace(/\s*\([^)]*\)/g, '').trim();
  60. }
  61.  
  62. // General async GM_xmlhttpRequest
  63. function gmRequest(details) {
  64. return new Promise((resolve, reject) => {
  65. GM_xmlhttpRequest({
  66. ...details,
  67. onload: function (response) {
  68. resolve(response);
  69. },
  70. onerror: function (error) {
  71. reject(error);
  72. }
  73. });
  74. });
  75. }
  76.  
  77. // ===== 3. Style injection =====
  78. function addCustomStyles() {
  79. const style = document.createElement('style');
  80. style.innerHTML = `
  81. html {
  82. width: 100% !important;
  83. margin: 0 !important;
  84. }
  85. @media screen and (min-width:800px) {
  86. html {
  87. width: 100% !important;
  88. margin: 0 !important;
  89. }
  90. body {
  91. border: none !important;
  92. padding: 0 20px !important;
  93. box-shadow: none !important;
  94. border-radius: 0 !important;
  95. }
  96. }
  97. /* Management panel style */
  98. #keywordManager {
  99. max-height: 50px;
  100. transition: max-height 0.3s ease-out;
  101. overflow: hidden;
  102. position: fixed;
  103. top: 20px;
  104. right: 20px;
  105. background-color: #ffffff;
  106. border: 1px solid #ccc;
  107. padding: 20px;
  108. z-index: 1000;
  109. width: 400px;
  110. box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
  111. border-radius: 8px;
  112. font-family: Arial, sans-serif;
  113. font-size: 14px;
  114. line-height: 1.5;
  115. color: #333;
  116. }
  117. #keywordManager.expanded {
  118. max-height: 800px; /* Large enough to fit content */
  119. }
  120. /* Progress bar style */
  121. #progressOverlay {
  122. position: fixed;
  123. top: 0;
  124. left: 0;
  125. width: 100%;
  126. height: 100%;
  127. background: rgba(0, 0, 0, 0.5);
  128. display: flex;
  129. justify-content: center;
  130. align-items: center;
  131. z-index: 3000;
  132. }
  133. #progressBarContainer {
  134. width: 80%;
  135. background: #fff;
  136. padding: 20px;
  137. border-radius: 8px;
  138. text-align: center;
  139. }
  140. #progressBar {
  141. width: 100%;
  142. background: #e0e0e0;
  143. border-radius: 4px;
  144. overflow: hidden;
  145. margin-top: 10px;
  146. }
  147. #progressBar div {
  148. height: 20px;
  149. width: 0;
  150. background: #28a745;
  151. transition: width 0.3s;
  152. }
  153. /* Floating button style */
  154. .floating-button {
  155. position: fixed;
  156. display: none;
  157. z-index: 1000;
  158. padding: 5px 10px;
  159. border: none;
  160. border-radius: 4px;
  161. cursor: pointer;
  162. font-size: 12px;
  163. }
  164. #filterButton {
  165. background-color: #dc3545;
  166. color: #fff;
  167. }
  168. #filterButton:hover {
  169. background-color: #c82333;
  170. }
  171. #favoriteButton {
  172. background-color: #28a745;
  173. color: #fff;
  174. }
  175. #favoriteButton:hover {
  176. background-color: #218838;
  177. }
  178. #knownButton {
  179. background-color: #17a2b8;
  180. color: #fff;
  181. }
  182. #knownButton:hover {
  183. background-color: #117a8b;
  184. }
  185. #watchedButton {
  186. background-color: #007bff;
  187. color: #fff;
  188. }
  189. #watchedButton:hover {
  190. background-color: #0056b3;
  191. }
  192. /* Settings button style */
  193. #settingsButton {
  194. padding: 5px 10px;
  195. background-color: #ffc107;
  196. color: #fff;
  197. border: none;
  198. border-radius: 4px;
  199. cursor: pointer;
  200. font-size: 12px;
  201. position: absolute;
  202. bottom: 20px;
  203. right: 20px;
  204. }
  205. #settingsButton:hover {
  206. background-color: #e0a800;
  207. }
  208. /* Settings modal style */
  209. #settingsModal {
  210. position: fixed; /* changed to fixed positioning */
  211. background-color: #fff;
  212. padding: 20px;
  213. border: 1px solid #ccc;
  214. border-radius: 8px;
  215. z-index: 4000;
  216. box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
  217. display: none;
  218. width: 250px;
  219. max-width: 90vw;
  220. max-height: 90vh;
  221. }
  222. #settingsModal h4 {
  223. margin-top: 0;
  224. }
  225. #settingsModal label {
  226. display: block;
  227. margin-bottom: 10px;
  228. }
  229. #settingsModal button.close-settings {
  230. padding: 5px 10px;
  231. margin-top: 10px;
  232. background-color: #dc3545;
  233. color: #fff;
  234. border: none;
  235. border-radius: 4px;
  236. cursor: pointer;
  237. }
  238. #settingsModal button.close-settings:hover {
  239. background-color: #c82333;
  240. }
  241. `;
  242. document.head.appendChild(style);
  243. }
  244. addCustomStyles(); // Inject styles immediately
  245.  
  246. // ===== 4. Create section function =====
  247. function createSection(sectionId, sectionTitle, storageKey) {
  248. let section = document.createElement('div');
  249. section.id = sectionId;
  250.  
  251. let title = document.createElement('h4');
  252. title.textContent = sectionTitle;
  253. title.style.marginTop = '10px';
  254. title.style.marginBottom = '5px';
  255. title.style.color = '#444';
  256. section.appendChild(title);
  257.  
  258. let inputContainer = document.createElement('div');
  259. inputContainer.style.display = 'flex';
  260. inputContainer.style.marginBottom = '10px';
  261.  
  262. let input = document.createElement('input');
  263. input.type = 'text';
  264. input.placeholder = '添加关键词';
  265. input.style.flex = '1';
  266. input.style.padding = '5px';
  267. input.style.border = '1px solid #ccc';
  268. input.style.borderRadius = '4px';
  269.  
  270. let addButton = document.createElement('button');
  271. addButton.textContent = '添加';
  272. addButton.style.padding = '5px 10px';
  273. addButton.style.marginLeft = '5px';
  274. addButton.style.backgroundColor = '#28a745';
  275. addButton.style.color = '#fff';
  276. addButton.style.border = 'none';
  277. addButton.style.borderRadius = '4px';
  278. addButton.style.cursor = 'pointer';
  279. addButton.onmouseover = function () {
  280. addButton.style.backgroundColor = '#218838';
  281. };
  282. addButton.onmouseout = function () {
  283. addButton.style.backgroundColor = '#28a745';
  284. };
  285. addButton.onclick = function (event) {
  286. event.stopPropagation(); // Prevent triggering panel toggle
  287. let keyword = input.value.trim();
  288. if (keyword) {
  289. addKeyword(keyword, storageKey, sectionId);
  290. input.value = '';
  291. }
  292. };
  293.  
  294. inputContainer.appendChild(input);
  295. inputContainer.appendChild(addButton);
  296. section.appendChild(inputContainer);
  297.  
  298. // Keyword list
  299. let list = document.createElement('ul');
  300. list.style.listStyleType = 'none';
  301. list.style.padding = '0';
  302. list.style.maxHeight = '200px';
  303. list.style.overflowY = 'auto';
  304. section.appendChild(list);
  305.  
  306. // Bottom container, containing batch import button and feature toggle
  307. let bottomContainer = document.createElement('div');
  308. bottomContainer.style.display = 'flex';
  309. bottomContainer.style.alignItems = 'center';
  310.  
  311. // Batch import button
  312. let batchImportButton = document.createElement('button');
  313. batchImportButton.textContent = '批量导入';
  314. batchImportButton.style.padding = '5px 10px';
  315. batchImportButton.style.backgroundColor = '#17a2b8';
  316. batchImportButton.style.color = '#fff';
  317. batchImportButton.style.border = 'none';
  318. batchImportButton.style.borderRadius = '4px';
  319. batchImportButton.style.cursor = 'pointer';
  320. batchImportButton.style.marginRight = '10px';
  321. batchImportButton.onmouseover = function () {
  322. batchImportButton.style.backgroundColor = '#117a8b';
  323. };
  324. batchImportButton.onmouseout = function () {
  325. batchImportButton.style.backgroundColor = '#17a2b8';
  326. };
  327. batchImportButton.onclick = function (event) {
  328. event.stopPropagation(); // Prevent triggering panel toggle
  329. showBatchImportModal(storageKey, sectionId);
  330. };
  331. bottomContainer.appendChild(batchImportButton);
  332.  
  333. // If it is the "屏蔽" section, add an enable filter toggle
  334. if (storageKey === STORAGE_KEY) {
  335. let filterToggleContainer = document.createElement('div');
  336. filterToggleContainer.style.display = 'flex';
  337. filterToggleContainer.style.alignItems = 'center';
  338. filterToggleContainer.style.marginLeft = '10px';
  339.  
  340. let filterToggleSwitch = document.createElement('input');
  341. filterToggleSwitch.type = 'checkbox';
  342. filterToggleSwitch.checked = getFilterEnabled();
  343. filterToggleSwitch.onchange = function () {
  344. GM_setValue(FILTER_TOGGLE_KEY, filterToggleSwitch.checked);
  345. modifyPage(); // Re-process the page
  346. };
  347. filterToggleContainer.appendChild(filterToggleSwitch);
  348.  
  349. let filterToggleLabel = document.createElement('label');
  350. filterToggleLabel.textContent = ' 启用屏蔽';
  351. filterToggleLabel.style.marginLeft = '5px';
  352. filterToggleContainer.appendChild(filterToggleLabel);
  353.  
  354. bottomContainer.appendChild(filterToggleContainer);
  355. }
  356. section.appendChild(bottomContainer);
  357. return section;
  358. }
  359.  
  360. // ===== 5. Keyword management panel (UI skeleton) =====
  361. function createKeywordManager() {
  362. let manager = document.createElement('div');
  363. manager.id = 'keywordManager';
  364.  
  365. // Title bar
  366. let title = document.createElement('div');
  367. title.style.display = 'flex';
  368. title.style.justifyContent = 'space-between';
  369. title.style.alignItems = 'center';
  370. title.style.cursor = 'pointer';
  371.  
  372. let titleText = document.createElement('h3');
  373. titleText.textContent = '管理面板';
  374. titleText.style.fontSize = '16px';
  375. titleText.style.color = '#444';
  376. titleText.style.margin = '0';
  377.  
  378. let titleRightContainer = document.createElement('div');
  379. titleRightContainer.style.display = 'flex';
  380. titleRightContainer.style.alignItems = 'center';
  381.  
  382. // Export button
  383. let exportButton = document.createElement('button');
  384. exportButton.textContent = '导出关键词';
  385. exportButton.id = 'exportKeywordsButton';
  386. exportButton.style.padding = '5px 10px';
  387. exportButton.style.backgroundColor = '#007bff';
  388. exportButton.style.color = '#fff';
  389. exportButton.style.border = 'none';
  390. exportButton.style.borderRadius = '4px';
  391. exportButton.style.cursor = 'pointer';
  392. exportButton.style.fontSize = '12px';
  393. exportButton.style.marginRight = '10px';
  394. exportButton.onmouseover = function () {
  395. exportButton.style.backgroundColor = '#0056b3';
  396. };
  397. exportButton.onmouseout = function () {
  398. exportButton.style.backgroundColor = '#007bff';
  399. };
  400. exportButton.onclick = function (event) {
  401. event.stopPropagation();
  402. exportKeywordsToCSV();
  403. };
  404. titleRightContainer.appendChild(exportButton);
  405.  
  406. // Load/hide actress info button
  407. let loadActorsButton = document.createElement('button');
  408. loadActorsButton.textContent = '加载演员信息';
  409. loadActorsButton.id = 'loadActorsButton';
  410. loadActorsButton.style.padding = '5px 10px';
  411. loadActorsButton.style.backgroundColor = '#6c757d';
  412. loadActorsButton.style.color = '#fff';
  413. loadActorsButton.style.border = 'none';
  414. loadActorsButton.style.borderRadius = '4px';
  415. loadActorsButton.style.cursor = 'pointer';
  416. loadActorsButton.style.fontSize = '12px';
  417. loadActorsButton.style.marginLeft = '0px';
  418. loadActorsButton.onmouseover = function () {
  419. loadActorsButton.style.backgroundColor = '#5a6268';
  420. };
  421. loadActorsButton.onmouseout = function () {
  422. loadActorsButton.style.backgroundColor = '#6c757d';
  423. };
  424. loadActorsButton.onclick = function (event) {
  425. event.stopPropagation();
  426. toggleActorsColumn();
  427. };
  428. titleRightContainer.appendChild(loadActorsButton);
  429.  
  430. let toggleIcon = document.createElement('span');
  431. toggleIcon.textContent = '▼';
  432. toggleIcon.style.fontSize = '18px';
  433. toggleIcon.style.marginLeft = '10px';
  434. titleRightContainer.appendChild(toggleIcon);
  435.  
  436. title.appendChild(titleText);
  437. title.appendChild(titleRightContainer);
  438. title.onclick = toggleManager;
  439. manager.appendChild(title);
  440.  
  441. // Navigation bar
  442. let nav = document.createElement('div');
  443. nav.style.display = 'flex';
  444. nav.style.marginTop = '10px';
  445.  
  446. // Create navigation buttons
  447. let favoriteTab = createNavButton('喜爱', 'favorite');
  448. let knownTab = createNavButton('认识', 'known');
  449. let filterTab = createNavButton('屏蔽', 'filter');
  450. let watchedTab = createNavButton('看过', 'watched'); // Newly added
  451.  
  452. nav.appendChild(favoriteTab);
  453. nav.appendChild(knownTab);
  454. nav.appendChild(filterTab);
  455. nav.appendChild(watchedTab);
  456.  
  457. manager.appendChild(nav);
  458.  
  459. // Create 4 sections
  460. let favoriteSection = createSection('favorite', '喜爱', FAVORITES_KEY);
  461. let knownSection = createSection('known', '认识', KNOWN_KEY);
  462. let filterSection = createSection('filter', '屏蔽', STORAGE_KEY);
  463. let watchedSection = createSection('watched', '看过', WATCHED_KEY);
  464.  
  465. // Initially hide all except "favorite"
  466. knownSection.style.display = 'none';
  467. filterSection.style.display = 'none';
  468. watchedSection.style.display = 'none';
  469.  
  470. manager.appendChild(favoriteSection);
  471. manager.appendChild(knownSection);
  472. manager.appendChild(filterSection);
  473. manager.appendChild(watchedSection);
  474.  
  475. // Settings button
  476. let settingsButton = document.createElement('button');
  477. settingsButton.id = 'settingsButton';
  478. settingsButton.textContent = '设置';
  479. settingsButton.style.display = 'none'; // Default hidden
  480. manager.appendChild(settingsButton);
  481.  
  482. // Create settings modal
  483. let settingsModal = document.createElement('div');
  484. settingsModal.id = 'settingsModal';
  485.  
  486. let settingsTitle = document.createElement('h4');
  487. settingsTitle.textContent = '排序设置';
  488. settingsModal.appendChild(settingsTitle);
  489.  
  490. // Sort by priority
  491. let priorityLabel = document.createElement('label');
  492. let priorityCheckbox = document.createElement('input');
  493. priorityCheckbox.type = 'checkbox';
  494. priorityCheckbox.checked = getSortPriorityEnabled();
  495. priorityCheckbox.onchange = function () {
  496. GM_setValue(SORT_PRIORITY_KEY, priorityCheckbox.checked);
  497. modifyPage();
  498. };
  499. priorityLabel.appendChild(priorityCheckbox);
  500. priorityLabel.appendChild(document.createTextNode(' 按照喜爱/认识/看过排序'));
  501. settingsModal.appendChild(priorityLabel);
  502.  
  503. // Sort by actress info
  504. let actorSortLabel = document.createElement('label');
  505. let actorSortCheckbox = document.createElement('input');
  506. actorSortCheckbox.type = 'checkbox';
  507. actorSortCheckbox.checked = getSortActorEnabled();
  508. actorSortCheckbox.onchange = function () {
  509. GM_setValue(SORT_ACTOR_KEY, actorSortCheckbox.checked);
  510. if (actorSortCheckbox.checked) {
  511. // Automatically load actress info
  512. GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, true);
  513. }
  514. modifyPage();
  515. };
  516. actorSortLabel.appendChild(actorSortCheckbox);
  517. actorSortLabel.appendChild(document.createTextNode(' 按照演员信息排序'));
  518. settingsModal.appendChild(actorSortLabel);
  519.  
  520. // Close button
  521. let closeButton = document.createElement('button');
  522. closeButton.textContent = '关闭';
  523. closeButton.classList.add('close-settings');
  524. closeButton.onclick = function () {
  525. settingsModal.style.display = 'none';
  526. };
  527. settingsModal.appendChild(closeButton);
  528.  
  529. document.body.appendChild(settingsModal);
  530.  
  531. settingsButton.onclick = function (event) {
  532. event.stopPropagation();
  533. // Calculate modal position
  534. const rect = settingsButton.getBoundingClientRect();
  535. const modalWidth = 250;
  536. const modalHeight = settingsModal.offsetHeight || 200;
  537.  
  538. let top = rect.bottom + 5;
  539. let left = rect.left;
  540.  
  541. if (left + modalWidth > window.innerWidth) {
  542. left = window.innerWidth - modalWidth - 10;
  543. }
  544. if (top + modalHeight > window.innerHeight) {
  545. top = rect.top - modalHeight - 5;
  546. }
  547. if (left < 10) left = 10;
  548. if (top < 10) top = 10;
  549.  
  550. settingsModal.style.top = `${top}px`;
  551. settingsModal.style.left = `${left}px`;
  552. settingsModal.style.display = 'block';
  553. };
  554.  
  555. // Click outside the modal to close
  556. window.onclick = function (event) {
  557. if (event.target === settingsModal) {
  558. settingsModal.style.display = 'none';
  559. }
  560. };
  561.  
  562. document.body.appendChild(manager);
  563.  
  564. // Initialize list content for each section
  565. updateKeywordList(FAVORITES_KEY, 'favorite');
  566. updateKeywordList(KNOWN_KEY, 'known');
  567. updateKeywordList(STORAGE_KEY, 'filter');
  568. updateKeywordList(WATCHED_KEY, 'watched');
  569.  
  570. // Panel expand/collapse
  571. function toggleManager() {
  572. if (manager.classList.contains('expanded')) {
  573. manager.classList.remove('expanded');
  574. toggleIcon.textContent = '▼';
  575. settingsButton.style.display = 'none';
  576. } else {
  577. manager.classList.add('expanded');
  578. toggleIcon.textContent = '▲';
  579. settingsButton.style.display = 'block';
  580. }
  581. }
  582.  
  583. function createNavButton(text, sectionId) {
  584. let button = document.createElement('button');
  585. button.textContent = text;
  586. button.style.flex = '1';
  587. button.style.padding = '5px';
  588. button.style.border = 'none';
  589. button.style.backgroundColor = '#f8f9fa';
  590. button.style.color = '#007bff';
  591. button.style.cursor = 'pointer';
  592. button.style.borderRadius = '4px';
  593. button.style.marginRight = '5px';
  594. button.onclick = function () { switchSection(sectionId); };
  595. return button;
  596. }
  597.  
  598. function switchSection(section) {
  599. let tabs = {
  600. favorite: favoriteTab,
  601. known: knownTab,
  602. filter: filterTab,
  603. watched: watchedTab
  604. };
  605. let sections = {
  606. favorite: favoriteSection,
  607. known: knownSection,
  608. filter: filterSection,
  609. watched: watchedSection
  610. };
  611. for (let key in tabs) {
  612. if (key === section) {
  613. tabs[key].style.backgroundColor = '#007bff';
  614. tabs[key].style.color = '#fff';
  615. sections[key].style.display = 'block';
  616. } else {
  617. tabs[key].style.backgroundColor = '#f8f9fa';
  618. tabs[key].style.color = '#007bff';
  619. sections[key].style.display = 'none';
  620. }
  621. }
  622. }
  623. }
  624. /*******************************
  625. * Block 2
  626. * 6. Keyword management functions
  627. * 7. SQL handling functions
  628. * 10. Batch import and export functions
  629. *******************************/
  630.  
  631. // ===== 6. Keyword management functions =====
  632. function getStoredKeywords(storageKey) {
  633. return GM_getValue(storageKey, []);
  634. }
  635. function getFilterEnabled() {
  636. return GM_getValue(FILTER_TOGGLE_KEY, false);
  637. }
  638. function getSortPriorityEnabled() {
  639. return GM_getValue(SORT_PRIORITY_KEY, false);
  640. }
  641. function getSortActorEnabled() {
  642. return GM_getValue(SORT_ACTOR_KEY, false);
  643. }
  644.  
  645. function saveKeywords(keywords, storageKey, sectionId) {
  646. GM_setValue(storageKey, keywords);
  647. updateKeywordList(storageKey, sectionId);
  648. modifyPage(); // Update highlight
  649. }
  650.  
  651. function addKeyword(keyword, storageKey, sectionId) {
  652. let keywords = getStoredKeywords(storageKey);
  653. if (!keywords.includes(keyword)) {
  654. // Ensure exclusivity across lists
  655. if (storageKey === FAVORITES_KEY) {
  656. removeKeywordByValue(keyword, KNOWN_KEY, 'known');
  657. removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
  658. removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
  659. } else if (storageKey === KNOWN_KEY) {
  660. removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
  661. removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
  662. } else if (storageKey === WATCHED_KEY) {
  663. removeKeywordByValue(keyword, FAVORITES_KEY, 'favorite');
  664. removeKeywordByValue(keyword, KNOWN_KEY, 'known');
  665. removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
  666. }
  667. keywords.push(keyword);
  668. saveKeywords(keywords, storageKey, sectionId);
  669. } else {
  670. alert('关键词已存在');
  671. }
  672. }
  673.  
  674. function removeKeyword(index, storageKey, sectionId) {
  675. let keywords = getStoredKeywords(storageKey);
  676. keywords.splice(index, 1);
  677. saveKeywords(keywords, storageKey, sectionId);
  678. }
  679.  
  680. function removeKeywordByValue(keyword, storageKey, sectionId) {
  681. let keywords = getStoredKeywords(storageKey);
  682. let idx = keywords.indexOf(keyword);
  683. if (idx !== -1) {
  684. keywords.splice(idx, 1);
  685. saveKeywords(keywords, storageKey, sectionId);
  686. }
  687. }
  688.  
  689. function updateKeywordList(storageKey, sectionId) {
  690. let keywords = getStoredKeywords(storageKey);
  691. let section = document.getElementById(sectionId);
  692. if (!section) return;
  693. let list = section.querySelector('ul');
  694. list.innerHTML = '';
  695.  
  696. keywords.forEach((keyword, index) => {
  697. let listItem = document.createElement('li');
  698. listItem.style.display = 'flex';
  699. listItem.style.justifyContent = 'space-between';
  700. listItem.style.alignItems = 'center';
  701. listItem.style.padding = '5px 0';
  702.  
  703. let keywordText = document.createElement('span');
  704. keywordText.textContent = keyword;
  705.  
  706. let buttonContainer = document.createElement('div');
  707.  
  708. let editButton = document.createElement('button');
  709. editButton.textContent = '编辑';
  710. editButton.style.marginRight = '5px';
  711. editButton.style.padding = '2px 5px';
  712. editButton.style.backgroundColor = '#ffc107';
  713. editButton.style.color = '#fff';
  714. editButton.style.border = 'none';
  715. editButton.style.borderRadius = '4px';
  716. editButton.style.cursor = 'pointer';
  717. editButton.style.fontSize = '12px';
  718. editButton.onmouseover = function () {
  719. editButton.style.backgroundColor = '#e0a800';
  720. };
  721. editButton.onmouseout = function () {
  722. editButton.style.backgroundColor = '#ffc107';
  723. };
  724. editButton.onclick = function (event) {
  725. event.stopPropagation();
  726. showEditInput(index, keyword, storageKey, sectionId);
  727. };
  728.  
  729. let deleteButton = document.createElement('button');
  730. deleteButton.textContent = '删除';
  731. deleteButton.style.padding = '2px 5px';
  732. deleteButton.style.backgroundColor = '#dc3545';
  733. deleteButton.style.color = '#fff';
  734. deleteButton.style.border = 'none';
  735. deleteButton.style.borderRadius = '4px';
  736. deleteButton.style.cursor = 'pointer';
  737. deleteButton.style.fontSize = '12px';
  738. deleteButton.onmouseover = function () {
  739. deleteButton.style.backgroundColor = '#c82333';
  740. };
  741. deleteButton.onmouseout = function () {
  742. deleteButton.style.backgroundColor = '#dc3545';
  743. };
  744. deleteButton.onclick = function (event) {
  745. event.stopPropagation();
  746. removeKeyword(index, storageKey, sectionId);
  747. };
  748.  
  749. buttonContainer.appendChild(editButton);
  750. buttonContainer.appendChild(deleteButton);
  751.  
  752. listItem.appendChild(keywordText);
  753. listItem.appendChild(buttonContainer);
  754. list.appendChild(listItem);
  755. });
  756. }
  757.  
  758. function showEditInput(index, oldKeyword, storageKey, sectionId) {
  759. let newKeyword = prompt('编辑关键词', oldKeyword);
  760. if (newKeyword !== null && newKeyword.trim() !== '') {
  761. editKeyword(index, newKeyword.trim(), storageKey, sectionId);
  762. }
  763. }
  764.  
  765. function editKeyword(index, newKeyword, storageKey, sectionId) {
  766. let keywords = getStoredKeywords(storageKey);
  767. if (!keywords.includes(newKeyword)) {
  768. // Ensure uniqueness
  769. if (storageKey === FAVORITES_KEY) {
  770. removeKeywordByValue(newKeyword, KNOWN_KEY, 'known');
  771. removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter');
  772. removeKeywordByValue(newKeyword, WATCHED_KEY, 'watched');
  773. } else if (storageKey === KNOWN_KEY) {
  774. removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter');
  775. removeKeywordByValue(newKeyword, WATCHED_KEY, 'watched');
  776. } else if (storageKey === WATCHED_KEY) {
  777. removeKeywordByValue(newKeyword, FAVORITES_KEY, 'favorite');
  778. removeKeywordByValue(newKeyword, KNOWN_KEY, 'known');
  779. removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter');
  780. }
  781. keywords[index] = newKeyword;
  782. saveKeywords(keywords, storageKey, sectionId);
  783. } else {
  784. alert('关键词已存在');
  785. }
  786. }
  787.  
  788. // ===== 7. SQL handling functions =====
  789. function saveSQL() {
  790. const editor = document.querySelector('.CodeMirror')?.CodeMirror;
  791. if (editor) {
  792. const sql = editor.getValue(); // Get SQL content from CodeMirror editor
  793. GM_setValue(SQL_STORAGE_KEY, sql); // Save SQL to the specified storage key
  794. console.log('SQL 已保存:', sql);
  795. } else {
  796. console.error('未找到 CodeMirror 编辑器');
  797. }
  798. }
  799. function loadSQL() {
  800. return GM_getValue(SQL_STORAGE_KEY, '');
  801. }
  802.  
  803. // ===== 10. Batch import and export functions =====
  804. function showBatchImportModal(storageKey, sectionId) {
  805. let modal = document.createElement('div');
  806. modal.style.position = 'fixed';
  807. modal.style.top = '50%';
  808. modal.style.left = '50%';
  809. modal.style.transform = 'translate(-50%, -50%)';
  810. modal.style.backgroundColor = '#fff';
  811. modal.style.padding = '20px';
  812. modal.style.border = '1px solid #ccc';
  813. modal.style.borderRadius = '8px';
  814. modal.style.zIndex = '2000';
  815. modal.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.1)';
  816.  
  817. let textarea = document.createElement('textarea');
  818. textarea.placeholder = '请输入要导入的关键词,使用空格或换行分隔';
  819. textarea.style.width = '300px';
  820. textarea.style.height = '150px';
  821. textarea.style.marginBottom = '10px';
  822. modal.appendChild(textarea);
  823.  
  824. let buttonContainer = document.createElement('div');
  825. buttonContainer.style.display = 'flex';
  826. buttonContainer.style.justifyContent = 'space-between';
  827.  
  828. let importButton = document.createElement('button');
  829. importButton.textContent = '导入';
  830. importButton.style.padding = '5px 10px';
  831. importButton.style.backgroundColor = '#28a745';
  832. importButton.style.color = '#fff';
  833. importButton.style.border = 'none';
  834. importButton.style.borderRadius = '4px';
  835. importButton.style.cursor = 'pointer';
  836. importButton.onmouseover = function () {
  837. importButton.style.backgroundColor = '#218838';
  838. };
  839. importButton.onmouseout = function () {
  840. importButton.style.backgroundColor = '#28a745';
  841. };
  842. importButton.onclick = function (event) {
  843. event.stopPropagation();
  844. let inputText = textarea.value.trim();
  845. if (inputText) {
  846. let newKeywords = inputText.split(/\s+|\n+/);
  847. let keywords = getStoredKeywords(storageKey);
  848. let duplicates = [];
  849. newKeywords.forEach(keyword => {
  850. keyword = keyword.trim();
  851. if (keyword) {
  852. if (!keywords.includes(keyword)) {
  853. if (storageKey === FAVORITES_KEY) {
  854. removeKeywordByValue(keyword, KNOWN_KEY, 'known');
  855. removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
  856. removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
  857. } else if (storageKey === KNOWN_KEY) {
  858. removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
  859. removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
  860. } else if (storageKey === WATCHED_KEY) {
  861. removeKeywordByValue(keyword, FAVORITES_KEY, 'favorite');
  862. removeKeywordByValue(keyword, KNOWN_KEY, 'known');
  863. removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
  864. }
  865. keywords.push(keyword);
  866. } else {
  867. duplicates.push(keyword);
  868. }
  869. }
  870. });
  871. saveKeywords(keywords, storageKey, sectionId);
  872. if (duplicates.length > 0) {
  873. alert(`以下关键词已存在于 "${sectionId}" 列表中:\n${duplicates.join(', ')}`);
  874. }
  875. }
  876. document.body.removeChild(modal);
  877. };
  878. buttonContainer.appendChild(importButton);
  879.  
  880. let cancelButton = document.createElement('button');
  881. cancelButton.textContent = '取消';
  882. cancelButton.style.padding = '5px 10px';
  883. cancelButton.style.backgroundColor = '#dc3545';
  884. cancelButton.style.color = '#fff';
  885. cancelButton.style.border = 'none';
  886. cancelButton.style.borderRadius = '4px';
  887. cancelButton.style.cursor = 'pointer';
  888. cancelButton.onmouseover = function () {
  889. cancelButton.style.backgroundColor = '#c82333';
  890. };
  891. cancelButton.onmouseout = function () {
  892. cancelButton.style.backgroundColor = '#dc3545';
  893. };
  894. cancelButton.onclick = function (event) {
  895. event.stopPropagation();
  896. document.body.removeChild(modal);
  897. };
  898. buttonContainer.appendChild(cancelButton);
  899.  
  900. modal.appendChild(buttonContainer);
  901. document.body.appendChild(modal);
  902. }
  903.  
  904. function exportKeywordsToCSV() {
  905. let favorites = getStoredKeywords(FAVORITES_KEY);
  906. let knowns = getStoredKeywords(KNOWN_KEY);
  907. let filters = getStoredKeywords(STORAGE_KEY);
  908. let watched = getStoredKeywords(WATCHED_KEY);
  909.  
  910. let maxLength = Math.max(favorites.length, knowns.length, filters.length, watched.length);
  911. let bom = '\uFEFF';
  912. let csvContent = bom + '喜爱,认识,看过,屏蔽\n';
  913.  
  914. for (let i = 0; i < maxLength; i++) {
  915. let fav = favorites[i] ? `"${favorites[i]}"` : '';
  916. let kno = knowns[i] ? `"${knowns[i]}"` : '';
  917. let wat = watched[i] ? `"${watched[i]}"` : '';
  918. let fil = filters[i] ? `"${filters[i]}"` : '';
  919. csvContent += `${fav},${kno},${wat},${fil}\n`;
  920. }
  921.  
  922. GM_download({
  923. url: 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent),
  924. name: 'keywords.csv',
  925. saveAs: true
  926. });
  927. }
  928.  
  929. /*******************************
  930. * Block 3
  931. * 8. Actress info extraction
  932. * 9. Actress info crawling
  933. * 11. Actress info insertion and hiding
  934. *******************************/
  935.  
  936. // ===== 8. Actress info extraction (parse HTML) =====
  937. async function parseActors(doc, source, code) {
  938. let actors = [];
  939. if (source === 'javbus.com') {
  940. let starElements = doc.querySelectorAll('div.star-box-common li div.star-name a');
  941. starElements.forEach(a => {
  942. let actorName = a.textContent.trim();
  943. if (actorName) {
  944. actors.push(stripParentheses(actorName));
  945. }
  946. });
  947. console.log(`[parseActors] javbus.com: Found actresses:`, actors);
  948. return actors;
  949. } else if (source === 'javmenu.com') {
  950. let actressLinks = doc.querySelectorAll('a.actress');
  951. actressLinks.forEach(link => {
  952. let actorName = link.textContent.trim();
  953. if (actorName) {
  954. actors.push(stripParentheses(actorName));
  955. }
  956. });
  957. console.log(`[parseActors] javmenu.com: Found actresses:`, actors);
  958. return actors;
  959. } else if (source === 'jable.tv') {
  960. let modelsDiv = doc.querySelector('div.models');
  961. if (!modelsDiv) {
  962. console.warn(`[parseActors] jable.tv: Missing div.models element, returning empty array`);
  963. return actors;
  964. }
  965. let modelLinks = modelsDiv.querySelectorAll('a.model');
  966. modelLinks.forEach(link => {
  967. let span = link.querySelector('span.placeholder.rounded-circle');
  968. if (span) {
  969. let actorName = span.getAttribute('data-original-title') || span.getAttribute('title');
  970. if (actorName) {
  971. actors.push(stripParentheses(actorName.trim()));
  972. }
  973. }
  974. let img = link.querySelector('img.avatar.rounded-circle');
  975. if (img) {
  976. let actorName = img.getAttribute('data-original-title') || img.getAttribute('title');
  977. if (actorName) {
  978. actors.push(stripParentheses(actorName.trim()));
  979. }
  980. }
  981. });
  982. console.log(`[parseActors] jable.tv: Found actresses:`, actors);
  983. return actors;
  984. } else if (source === 'javdb.com') {
  985.  
  986. // 1) Check if code is provided
  987. if (!code) {
  988. console.warn('[parseActors] javdb.com: Code not provided, cannot parse actress info');
  989. return actors;
  990. }
  991.  
  992. // 2) Find the video detail link in search results
  993. let codeLink = Array.from(doc.querySelectorAll('a[href^="/v/"]')).find(link => {
  994. return link.textContent.trim().toUpperCase().includes(code.toUpperCase());
  995. });
  996. if (!codeLink) {
  997. console.warn(`[parseActors] javdb.com: No link /v/ containing ${code} found, returning empty array`);
  998. return actors;
  999. }
  1000. let href = codeLink.getAttribute('href');
  1001. let fullURL = `https://javdb.com${href}`;
  1002. console.log(`[parseActors] javdb.com: Found video link => ${fullURL}`);
  1003.  
  1004. // 3) Request the video detail page
  1005. try {
  1006. let response = await gmRequest({
  1007. method: 'GET',
  1008. url: fullURL,
  1009. headers: {
  1010. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
  1011. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  1012. 'Accept-Language': 'en-US,en;q=0.5',
  1013. 'Connection': 'keep-alive',
  1014. 'Referer': 'https://www.google.com/',
  1015. 'Upgrade-Insecure-Requests': '1',
  1016. 'Cache-Control': 'max-age=0'
  1017. }
  1018. });
  1019.  
  1020. // 4) Check return code
  1021. if (response.status === 200) {
  1022. console.log(`[parseActors] javdb.com: Got detail page successfully (status=200) => parse actress info now`);
  1023.  
  1024. // 5) Parse actress info
  1025. let parser = new DOMParser();
  1026. let detailedDoc = parser.parseFromString(response.responseText, 'text/html');
  1027. let panelBlocks = detailedDoc.querySelectorAll('div.panel-block');
  1028. // Note: "演員:" in Traditional Chinese
  1029. let actorDiv = Array.from(panelBlocks).find(block => {
  1030. let strong = block.querySelector('strong');
  1031. return strong && strong.textContent.trim() === '演員:';
  1032. });
  1033.  
  1034. if (!actorDiv) {
  1035. console.warn('[parseActors] javdb.com: No <strong>演員:</strong> panel-block found in detail page');
  1036. } else {
  1037. let actorLinks = actorDiv.querySelectorAll('span.value a[href^="/actors/"]');
  1038. actorLinks.forEach(link => {
  1039. let symbol = link.nextElementSibling;
  1040. // If male actors are needed, remove female check
  1041. if (symbol && symbol.classList.contains('symbol') && symbol.classList.contains('female')) {
  1042. let actorName = link.textContent.trim();
  1043. if (actorName) {
  1044. actors.push(stripParentheses(actorName));
  1045. }
  1046. }
  1047. });
  1048. }
  1049.  
  1050. } else {
  1051. console.warn(`[parseActors] javdb.com: Failed to get detail page, status=${response.status}, returning empty array`);
  1052. }
  1053. } catch (error) {
  1054. console.error(`[parseActors] javdb.com: Error fetching detail page ${fullURL}:`, error);
  1055. }
  1056.  
  1057. console.log(`[parseActors] javdb.com: Final actress list =>`, actors);
  1058. return actors;
  1059.  
  1060. } else {
  1061. // Unknown source
  1062. return actors;
  1063. }
  1064. }
  1065.  
  1066.  
  1067. // ===== 9. Actress info crawling =====
  1068. async function crawlMissingActors(missingActors) {
  1069. let progressOverlay = document.createElement('div');
  1070. progressOverlay.id = 'progressOverlay';
  1071. progressOverlay.style.position = 'fixed';
  1072. progressOverlay.style.top = '0';
  1073. progressOverlay.style.left = '0';
  1074. progressOverlay.style.width = '100%';
  1075. progressOverlay.style.height = '100%';
  1076. progressOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
  1077. progressOverlay.style.display = 'flex';
  1078. progressOverlay.style.justifyContent = 'center';
  1079. progressOverlay.style.alignItems = 'center';
  1080. progressOverlay.style.zIndex = '10000';
  1081.  
  1082. let progressBarContainer = document.createElement('div');
  1083. progressBarContainer.id = 'progressBarContainer';
  1084. progressBarContainer.style.width = '50%';
  1085. progressBarContainer.style.backgroundColor = '#fff';
  1086. progressBarContainer.style.padding = '20px';
  1087. progressBarContainer.style.borderRadius = '8px';
  1088. progressBarContainer.style.textAlign = 'center';
  1089.  
  1090. progressBarContainer.innerHTML = `
  1091. <h3>正在爬取演员信息...</h3>
  1092. <div id="progressBar" style="width: 100%; background-color: #ddd; border-radius: 5px; overflow: hidden; height: 20px; margin-bottom: 10px;">
  1093. <div style="width: 0%; height: 100%; background-color: #28a745;"></div>
  1094. </div>
  1095. <p id="progressText">0 / ${missingActors.length}</p>
  1096. `;
  1097. progressOverlay.appendChild(progressBarContainer);
  1098. document.body.appendChild(progressOverlay);
  1099.  
  1100. let progressBar = progressBarContainer.querySelector('#progressBar div');
  1101. let progressText = progressBarContainer.querySelector('#progressText');
  1102. let total = missingActors.length;
  1103. let completed = 0;
  1104.  
  1105. const concurrency = 4;
  1106. let current = 0;
  1107. let activeRequests = 0;
  1108.  
  1109. async function crawl() {
  1110. while (current < total && activeRequests < concurrency) {
  1111. fetchActorInfo(missingActors[current]);
  1112. current++;
  1113. }
  1114. if (completed >= total) {
  1115. document.body.removeChild(progressOverlay);
  1116. window.location.reload();
  1117. }
  1118. }
  1119.  
  1120. async function fetchActorInfo(code) {
  1121. activeRequests++;
  1122. let primarySources = [
  1123. { url: `https://www.javbus.com/${code}`, source: 'javbus.com' },
  1124. { url: `https://javdb.com/search?q=${code}`, source: 'javdb.com' },
  1125. { url: `https://jable.tv/videos/${code.toLowerCase()}/`, source: 'jable.tv' },
  1126. { url: `https://javmenu.com/en/${code}`, source: 'javmenu.com' },
  1127. ];
  1128. let fetched = false;
  1129.  
  1130. async function handleResponse(doc, source) {
  1131. try {
  1132. let actors = await parseActors(doc, source, code);
  1133. if (actors.length > 0) {
  1134. let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
  1135. numberActorData[code] = actors;
  1136. GM_setValue(NUMBER_ACTOR_STORAGE_KEY, numberActorData);
  1137. fetched = true;
  1138. completeFetch();
  1139. } else {
  1140. await tryNextSource();
  1141. }
  1142. } catch (error) {
  1143. console.error(`Error parsing actress info for code ${code}:`, error);
  1144. await tryNextSource();
  1145. }
  1146. }
  1147.  
  1148. async function tryNextSource() {
  1149. if (primarySources.length > 0) {
  1150. let nextSource = primarySources.shift();
  1151. let randomDelay = Math.floor(Math.random() * 1000) + 500;
  1152. await delay(randomDelay);
  1153.  
  1154. try {
  1155. let response = await gmRequest({
  1156. method: 'GET',
  1157. url: nextSource.url,
  1158. headers: {
  1159. 'User-Agent': getRandomUserAgent(),
  1160. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  1161. 'Accept-Language': 'en-US,en;q=0.5',
  1162. 'Connection': 'keep-alive',
  1163. 'Referer': 'https://www.google.com/',
  1164. 'Upgrade-Insecure-Requests': '1',
  1165. 'Cache-Control': 'max-age=0'
  1166. }
  1167. });
  1168. if (response.status === 200) {
  1169. let parser = new DOMParser();
  1170. let doc = parser.parseFromString(response.responseText, 'text/html');
  1171. await handleResponse(doc, nextSource.source);
  1172. } else {
  1173. await tryNextSource();
  1174. }
  1175. } catch (error) {
  1176. await tryNextSource();
  1177. }
  1178. } else if (!fetched) {
  1179. storeEmptyActorInfo(code);
  1180. completeFetch();
  1181. }
  1182. }
  1183.  
  1184. function completeFetch() {
  1185. completed++;
  1186. activeRequests--;
  1187. updateProgress();
  1188. crawl();
  1189. }
  1190.  
  1191. function storeEmptyActorInfo(code) {
  1192. let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
  1193. numberActorData[code] = [];
  1194. GM_setValue(NUMBER_ACTOR_STORAGE_KEY, numberActorData);
  1195. }
  1196.  
  1197. function getRandomUserAgent() {
  1198. const USER_AGENTS = [
  1199. 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
  1200. 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15',
  1201. 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
  1202. ];
  1203. return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
  1204. }
  1205.  
  1206. function delay(ms) {
  1207. return new Promise(resolve => setTimeout(resolve, ms));
  1208. }
  1209.  
  1210. await tryNextSource();
  1211. }
  1212.  
  1213. function updateProgress() {
  1214. progressBar.style.width = `${(completed / total) * 100}%`;
  1215. progressText.textContent = `${completed} / ${total}`;
  1216. }
  1217.  
  1218. crawl();
  1219. }
  1220.  
  1221. // ===== 11. Actress info insertion and hiding =====
  1222. function toggleActorsColumn() {
  1223. let shouldInsertActor = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT, false);
  1224. if (!shouldInsertActor) {
  1225. if (!GM_getValue(ACTORS_COLUMN_INSERTED_KEY)) {
  1226. insertActorsInfo();
  1227. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, true);
  1228. }
  1229. GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, true);
  1230. } else {
  1231. if (GM_getValue(ACTORS_COLUMN_INSERTED_KEY)) {
  1232. hideActorsInfo();
  1233. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
  1234. }
  1235. GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, false);
  1236. }
  1237. updateLoadActorsButton();
  1238. }
  1239.  
  1240. function updateLoadActorsButton() {
  1241. let loadActorsButton = document.getElementById('loadActorsButton');
  1242. if (!loadActorsButton) return;
  1243. let shouldInsert = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT);
  1244. loadActorsButton.textContent = shouldInsert ? '隐藏演员信息' : '加载演员信息';
  1245. }
  1246.  
  1247. function hideActorsInfo() {
  1248. if (isJinjierArt()) {
  1249. hideActorsColumnJinjierArt();
  1250. } else if (isJavdbRankings()) {
  1251. hideActorsInfoJavdb();
  1252. } else if (isJavLibrary()) {
  1253. hideActorsInfoJavLibrary();
  1254. }
  1255. }
  1256.  
  1257. function insertActorsInfo() {
  1258. if (isJinjierArt()) {
  1259. loadActorsAndInsertColumnJinjierArt();
  1260. } else if (isJavdbRankings()) {
  1261. loadActorsAndInsertInfoJavdb();
  1262. } else if (isJavLibrary()) {
  1263. loadActorsAndInsertInfoJavLibrary();
  1264. }
  1265. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, true);
  1266. }
  1267.  
  1268. function isJinjierArt() {
  1269. return window.location.hostname.includes('jinjier.art');
  1270. }
  1271.  
  1272. function isJavdbRankings() {
  1273. return window.location.hostname.includes('javdb.com') && !window.location.pathname.startsWith('/actors/') && !window.location.pathname.startsWith('/v/');
  1274. }
  1275.  
  1276. function isJavLibrary() {
  1277. // Simple check if domain contains "javlibrary.com"
  1278. return window.location.hostname.includes('javlibrary.com');
  1279. }
  1280.  
  1281. function hideActorsColumnJinjierArt() {
  1282. let table = document.querySelector('table');
  1283. if (!table) return;
  1284. let rows = table.querySelectorAll('tr');
  1285. rows.forEach(row => {
  1286. if (row.cells.length > 0) {
  1287. row.deleteCell(0);
  1288. }
  1289. });
  1290. }
  1291.  
  1292. function hideActorsInfoJavdb() {
  1293. let items = document.querySelectorAll('div.item');
  1294. items.forEach(item => {
  1295. let actorDiv = item.querySelector('.actors-info');
  1296. if (actorDiv) actorDiv.remove();
  1297. });
  1298. }
  1299.  
  1300. function hideActorsInfoJavLibrary() {
  1301. let videos = document.querySelectorAll('div.video');
  1302. videos.forEach(video => {
  1303. let actorDiv = video.querySelector('.actors-info');
  1304. if (actorDiv) {
  1305. actorDiv.remove();
  1306. }
  1307. });
  1308. }
  1309.  
  1310. function loadActorsAndInsertColumnJinjierArt() {
  1311. let codes = new Set();
  1312. let links = document.querySelectorAll('a');
  1313. links.forEach(link => {
  1314. let text = link.textContent.trim();
  1315. let codeMatch = text.match(/[A-Z]{2,5}-\d{2,5}/i);
  1316. if (codeMatch) {
  1317. codes.add(codeMatch[0].toUpperCase());
  1318. }
  1319. });
  1320.  
  1321. let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
  1322. let missingActors = [];
  1323. codes.forEach(code => {
  1324. if (!numberActorData[code] || !Array.isArray(numberActorData[code])) {
  1325. missingActors.push(code);
  1326. }
  1327. });
  1328. if (missingActors.length > 0) {
  1329. let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`);
  1330. if (proceed) {
  1331. crawlMissingActors(missingActors);
  1332. }
  1333. }
  1334.  
  1335. let codeActorMap = {};
  1336. codes.forEach(code => {
  1337. if (numberActorData[code] && Array.isArray(numberActorData[code])) {
  1338. codeActorMap[code] = numberActorData[code];
  1339. }
  1340. });
  1341.  
  1342. let table = document.querySelector('table');
  1343. if (!table) return;
  1344. let rows = table.querySelectorAll('tr');
  1345. rows.forEach((row) => {
  1346. let codeCell = row.querySelector('td a');
  1347. let actorsHTML = '';
  1348. if (codeCell) {
  1349. let codeText = codeCell.textContent.trim().toUpperCase();
  1350. let actors = codeActorMap[codeText] || [];
  1351. actorsHTML = actors.map(actor => `<div><a href="https://javdb.com/search?f=actor&locale=zh&q=${encodeURIComponent(actor)}" target="_blank">${stripParentheses(actor)}</a></div>`).join('');
  1352. }
  1353. let newCell = row.insertCell(0);
  1354. newCell.innerHTML = actorsHTML;
  1355. });
  1356. }
  1357.  
  1358. function loadActorsAndInsertInfoJavdb() {
  1359. let codes = new Set();
  1360. let items = document.querySelectorAll('div.item');
  1361. items.forEach(item => {
  1362. let videoTitle = item.querySelector('.video-title');
  1363. if (videoTitle) {
  1364. let strong = videoTitle.querySelector('strong');
  1365. if (strong) {
  1366. let code = strong.textContent.trim().toUpperCase();
  1367. let codeMatch = code.match(/[A-Z]{2,5}-\d{2,5}/i);
  1368. if (codeMatch) {
  1369. codes.add(codeMatch[0].toUpperCase());
  1370. }
  1371. }
  1372. }
  1373. });
  1374.  
  1375. let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
  1376. let missingActors = [];
  1377. codes.forEach(code => {
  1378. if (!numberActorData[code] || !Array.isArray(numberActorData[code])) {
  1379. missingActors.push(code);
  1380. }
  1381. });
  1382. if (missingActors.length > 0) {
  1383. let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`);
  1384. if (proceed) {
  1385. crawlMissingActors(missingActors);
  1386. }
  1387. }
  1388.  
  1389. let codeActorMap = {};
  1390. codes.forEach(code => {
  1391. if (numberActorData[code] && Array.isArray(numberActorData[code])) {
  1392. codeActorMap[code] = numberActorData[code];
  1393. }
  1394. });
  1395.  
  1396. let insertedItems = 0;
  1397. items.forEach(item => {
  1398. let videoTitle = item.querySelector('.video-title');
  1399. if (videoTitle) {
  1400. let strong = videoTitle.querySelector('strong');
  1401. if (strong) {
  1402. let codeText = strong.textContent.trim().toUpperCase();
  1403. let actors = codeActorMap[codeText] || [];
  1404. let actorsHTML = actors.map(actor => `<div><a href="https://javdb.com/search?f=actor&locale=zh&q=${encodeURIComponent(actor)}" target="_blank">${stripParentheses(actor)}</a></div>`).join('');
  1405. let actorDiv = document.createElement('div');
  1406. actorDiv.classList.add('actors-info');
  1407. actorDiv.innerHTML = `<strong>演员:</strong> ${actorsHTML}`;
  1408. actorDiv.style.marginBottom = '5px';
  1409. videoTitle.parentNode.insertBefore(actorDiv, videoTitle);
  1410. insertedItems++;
  1411. }
  1412. }
  1413. });
  1414. }
  1415.  
  1416. function loadActorsAndInsertInfoJavLibrary() {
  1417. // 1) Collect all codes
  1418. let codes = new Set();
  1419. let videoDivs = document.querySelectorAll('.video');
  1420.  
  1421. videoDivs.forEach(video => {
  1422. let codeElem = video.querySelector('.id');
  1423. if (codeElem) {
  1424. let codeText = codeElem.textContent.trim().toUpperCase();
  1425. let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i);
  1426. if (codeMatch) {
  1427. codes.add(codeMatch[0].toUpperCase());
  1428. }
  1429. }
  1430. });
  1431.  
  1432. // 2) Check existing number-actor mapping, see if any missing
  1433. let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
  1434. let missingActors = [];
  1435. codes.forEach(code => {
  1436. if (!numberActorData[code] || !Array.isArray(numberActorData[code])) {
  1437. missingActors.push(code);
  1438. }
  1439. });
  1440. if (missingActors.length > 0) {
  1441. let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`);
  1442. if (proceed) {
  1443. crawlMissingActors(missingActors);
  1444. }
  1445. }
  1446.  
  1447. // 3) Construct code => [actors] map
  1448. let codeActorMap = {};
  1449. codes.forEach(code => {
  1450. if (numberActorData[code] && Array.isArray(numberActorData[code])) {
  1451. codeActorMap[code] = numberActorData[code];
  1452. }
  1453. });
  1454.  
  1455. // 4) Insert actress info into each videoDiv
  1456. let insertedItems = 0;
  1457. videoDivs.forEach(video => {
  1458. let codeElem = video.querySelector('.id');
  1459. if (!codeElem) return;
  1460.  
  1461. let codeText = codeElem.textContent.trim().toUpperCase();
  1462. let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i);
  1463. if (!codeMatch) return;
  1464.  
  1465. let finalCode = codeMatch[0].toUpperCase();
  1466. let actors = codeActorMap[finalCode] || [];
  1467.  
  1468. if (actors.length > 0) {
  1469. let actorsHTML = actors.map(actor =>
  1470. `<div><a href="https://javdb.com/search?f=actor&locale=zh&q=${encodeURIComponent(actor)}" target="_blank">
  1471. ${stripParentheses(actor)}
  1472. </a></div>`
  1473. ).join('');
  1474.  
  1475. let actorDiv = document.createElement('div');
  1476. actorDiv.classList.add('actors-info');
  1477. actorDiv.innerHTML = `<strong>演员:</strong> ${actorsHTML}`;
  1478. actorDiv.style.marginBottom = '5px';
  1479.  
  1480. let titleElem = video.querySelector('.title');
  1481. if (titleElem) {
  1482. titleElem.insertAdjacentElement('afterend', actorDiv);
  1483. insertedItems++;
  1484. }
  1485. }
  1486. });
  1487.  
  1488. console.log('[loadActorsAndInsertInfoJavLibrary] Insert actress info successfully:', insertedItems, 'items');
  1489. }
  1490.  
  1491. /*******************************
  1492. * Block 4
  1493. * 12. Highlight and sorting
  1494. * 13. Floating buttons
  1495. * 14. modifyPage function
  1496. * 15. init function
  1497. *******************************/
  1498.  
  1499. // ===== 12. Highlight and sorting =====
  1500. function modifyPage() {
  1501. if (isSelectingText) return;
  1502. try {
  1503. let shouldInsertActors = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT, false);
  1504. let ActorsInserted = GM_getValue(ACTORS_COLUMN_INSERTED_KEY, false);
  1505. if (shouldInsertActors && !ActorsInserted) {
  1506. insertActorsInfo();
  1507. }
  1508. updateLoadActorsButton();
  1509.  
  1510. let favorites = getStoredKeywords(FAVORITES_KEY);
  1511. let knowns = getStoredKeywords(KNOWN_KEY);
  1512. let watched = getStoredKeywords(WATCHED_KEY);
  1513. let filters = getStoredKeywords(STORAGE_KEY);
  1514. let sortPriorityEnabled = getSortPriorityEnabled();
  1515. let sortActorEnabled = getSortActorEnabled();
  1516.  
  1517. if (isJinjierArt()) {
  1518. highlightRowsJinjierArt(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled);
  1519. } else if (isJavdbRankings()) {
  1520. highlightRowsJavdb(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled);
  1521. } else if (isJavLibrary()) {
  1522. highlightRowsJavLibrary(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled);
  1523. }
  1524. } catch (error) {
  1525. console.error('Error in modifyPage:', error);
  1526. }
  1527. }
  1528.  
  1529. function highlightRowsJinjierArt(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled) {
  1530. let rows = Array.from(document.querySelectorAll('table tr'));
  1531. let rowDataArray = [];
  1532.  
  1533. rows.forEach((row, rowIndex) => {
  1534. let cells = row.querySelectorAll('td');
  1535. let isFavorite = false;
  1536. let isKnown = false;
  1537. let isWatched = false;
  1538. let actors = [];
  1539.  
  1540. cells.forEach((cell, cellIndex) => {
  1541. if (!cell.hasAttribute('data-original-html')) {
  1542. cell.setAttribute('data-original-html', cell.innerHTML);
  1543. } else {
  1544. cell.innerHTML = cell.getAttribute('data-original-html');
  1545. }
  1546.  
  1547. let cellText = cell.textContent;
  1548. if (favorites.some(k => cellText.includes(k))) {
  1549. isFavorite = true;
  1550. }
  1551. if (knowns.some(k => cellText.includes(k))) {
  1552. isKnown = true;
  1553. }
  1554. if (watched.some(k => cellText.includes(k))) {
  1555. isWatched = true;
  1556. }
  1557.  
  1558. if (cellIndex === 0) {
  1559. let actorDivs = cell.querySelectorAll('div');
  1560. actorDivs.forEach(div => {
  1561. let actorName = div.textContent.trim();
  1562. if (actorName) actors.push(actorName);
  1563. });
  1564. }
  1565. });
  1566.  
  1567. if (isWatched) {
  1568. row.style.backgroundColor = 'lightblue';
  1569. } else if (isFavorite) {
  1570. row.style.backgroundColor = 'lightgreen';
  1571. } else if (isKnown) {
  1572. row.style.backgroundColor = 'yellow';
  1573. } else {
  1574. row.style.backgroundColor = '';
  1575. }
  1576.  
  1577. let shouldHide = false;
  1578. cells.forEach(cell => {
  1579. let cellText = cell.textContent;
  1580. if (filters.some(k => cellText.includes(k))) {
  1581. shouldHide = true;
  1582. }
  1583. });
  1584. shouldHide = shouldHide && getFilterEnabled();
  1585. row.style.display = shouldHide ? 'none' : '';
  1586.  
  1587. rowDataArray.push({
  1588. rowElement: row,
  1589. isWatched, isFavorite, isKnown,
  1590. actors,
  1591. originalIndex: rowIndex
  1592. });
  1593. });
  1594.  
  1595. if (sortPriorityEnabled || sortActorEnabled) {
  1596. rowDataArray.sort((a, b) => {
  1597. if (sortPriorityEnabled) {
  1598. if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1;
  1599. if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1;
  1600. if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1;
  1601. }
  1602. if (sortActorEnabled) {
  1603. let aActors = a.actors.join(', ') || '\uFFFF';
  1604. let bActors = b.actors.join(', ') || '\uFFFF';
  1605. if (aActors < bActors) return -1;
  1606. if (aActors > bActors) return 1;
  1607. }
  1608. return a.originalIndex - b.originalIndex;
  1609. });
  1610. }
  1611.  
  1612. let tbody = document.querySelector('table tbody');
  1613. if (tbody) {
  1614. tbody.innerHTML = '';
  1615. rowDataArray.forEach(rowData => {
  1616. tbody.appendChild(rowData.rowElement);
  1617. });
  1618. }
  1619. }
  1620.  
  1621. function highlightRowsJavdb(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled) {
  1622. let items = Array.from(document.querySelectorAll('div.movie-list.h.cols-4.vcols-8 > div.item'));
  1623. let itemDataArray = [];
  1624.  
  1625. items.forEach((item, itemIndex) => {
  1626. let videoTitleElem = item.querySelector('.video-title');
  1627. let isFavorite = false;
  1628. let isKnown = false;
  1629. let isWatched = false;
  1630. let actors = [];
  1631.  
  1632. if (videoTitleElem) {
  1633. let textContent = videoTitleElem.textContent;
  1634. if (favorites.some(k => textContent.includes(k))) {
  1635. isFavorite = true;
  1636. }
  1637. if (knowns.some(k => textContent.includes(k))) {
  1638. isKnown = true;
  1639. }
  1640. if (watched.some(k => textContent.includes(k))) {
  1641. isWatched = true;
  1642. }
  1643.  
  1644. let actorDiv = item.querySelector('.actors-info');
  1645. if (actorDiv) {
  1646. let actorLinks = actorDiv.querySelectorAll('a');
  1647. actorLinks.forEach(link => {
  1648. let actorName = link.textContent.trim();
  1649. if (actorName) actors.push(actorName);
  1650. });
  1651. }
  1652. }
  1653.  
  1654. let box = item.querySelector('a.box');
  1655. if (box) {
  1656. if (isWatched) {
  1657. box.style.backgroundColor = 'lightblue';
  1658. } else if (isFavorite) {
  1659. box.style.backgroundColor = 'lightgreen';
  1660. } else if (isKnown) {
  1661. box.style.backgroundColor = 'yellow';
  1662. } else {
  1663. box.style.backgroundColor = '';
  1664. }
  1665. }
  1666.  
  1667. let shouldHide = false;
  1668. if (box) {
  1669. let boxText = box.textContent;
  1670. if (filters.some(k => boxText.includes(k))) {
  1671. shouldHide = true;
  1672. }
  1673. }
  1674. shouldHide = shouldHide && getFilterEnabled();
  1675. item.style.display = shouldHide ? 'none' : '';
  1676.  
  1677. itemDataArray.push({
  1678. itemElement: item,
  1679. isWatched,
  1680. isFavorite,
  1681. isKnown,
  1682. actors,
  1683. originalIndex: itemIndex
  1684. });
  1685. });
  1686.  
  1687. if (sortPriorityEnabled || sortActorEnabled) {
  1688. itemDataArray.sort((a, b) => {
  1689. if (sortPriorityEnabled) {
  1690. if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1;
  1691. if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1;
  1692. if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1;
  1693. }
  1694. if (sortActorEnabled) {
  1695. let aActors = a.actors.join(', ') || '\uFFFF';
  1696. let bActors = b.actors.join(', ') || '\uFFFF';
  1697. if (aActors < bActors) return -1;
  1698. if (aActors > bActors) return 1;
  1699. }
  1700. return a.originalIndex - b.originalIndex;
  1701. });
  1702. }
  1703.  
  1704. let container = document.querySelector('div.movie-list.h.cols-4.vcols-8');
  1705. if (container) {
  1706. container.innerHTML = '';
  1707. itemDataArray.forEach(itemData => {
  1708. container.appendChild(itemData.itemElement);
  1709. });
  1710. }
  1711. }
  1712.  
  1713. function highlightRowsJavLibrary(
  1714. favorites,
  1715. knowns,
  1716. watched,
  1717. filters,
  1718. sortPriorityEnabled,
  1719. sortActorEnabled
  1720. ) {
  1721. // 0) Retrieve stored "code -> [actress array]" mapping
  1722. let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
  1723.  
  1724. // 1) Find the container that holds all video items (e.g. <div class="videos">)
  1725. let container = document.querySelector('.videos');
  1726. if (!container) {
  1727. console.warn('[highlightRowsJavLibrary] .videos container not found, exiting');
  1728. return;
  1729. }
  1730.  
  1731. // 2) Get all video elements, each is <div class="video" id="vid_XXXXX">
  1732. let items = Array.from(container.querySelectorAll('div.video'));
  1733.  
  1734. // 3) Collect item info for potential sorting
  1735. let itemDataArray = [];
  1736.  
  1737. items.forEach((item, index) => {
  1738. // 3.1) Get code and title
  1739. let codeElem = item.querySelector('.id');
  1740. let titleElem = item.querySelector('.title');
  1741.  
  1742. let codeText = codeElem ? codeElem.textContent.trim().toUpperCase() : '';
  1743. let titleText = titleElem ? titleElem.textContent.trim() : '';
  1744.  
  1745. // 3.2) Based on code, retrieve actress array from numberActorData
  1746. let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i);
  1747. let codeKey = codeMatch ? codeMatch[0].toUpperCase() : null;
  1748. let itemActors = (codeKey && Array.isArray(numberActorData[codeKey]))
  1749. ? numberActorData[codeKey]
  1750. : [];
  1751.  
  1752. // 3.3) Determine if this video matches favorites/known/watched by code/title
  1753. let isFavorite = false;
  1754. let isKnown = false;
  1755. let isWatched = false;
  1756. let shouldHide = false;
  1757.  
  1758. if (favorites.some(k => codeText.includes(k) || titleText.includes(k))) {
  1759. isFavorite = true;
  1760. }
  1761. if (knowns.some(k => codeText.includes(k) || titleText.includes(k))) {
  1762. isKnown = true;
  1763. }
  1764. if (watched.some(k => codeText.includes(k) || titleText.includes(k))) {
  1765. isWatched = true;
  1766. }
  1767. if (filters.some(k => codeText.includes(k) || titleText.includes(k))) {
  1768. shouldHide = true;
  1769. }
  1770.  
  1771. // 3.4) Check actress info for matches
  1772. let actorHasFavorite = itemActors.some(actor =>
  1773. favorites.some(k => actor.includes(k))
  1774. );
  1775. let actorHasKnown = itemActors.some(actor =>
  1776. knowns.some(k => actor.includes(k))
  1777. );
  1778. let actorHasWatched = itemActors.some(actor =>
  1779. watched.some(k => actor.includes(k))
  1780. );
  1781. let actorHasFilter = itemActors.some(actor =>
  1782. filters.some(k => actor.includes(k))
  1783. );
  1784.  
  1785. if (actorHasFavorite) isFavorite = true;
  1786. if (actorHasKnown) isKnown = true;
  1787. if (actorHasWatched) isWatched = true;
  1788.  
  1789. if (actorHasFilter) shouldHide = true;
  1790. // If the first three are not matched but there's a filter, set true
  1791. if (!isFavorite && !isKnown && !isWatched && shouldHide) {
  1792. shouldHide = true;
  1793. } else {
  1794. shouldHide = false;
  1795. }
  1796.  
  1797. // 3.5) Assign background color: watched > favorite > known
  1798. if (isWatched) {
  1799. item.style.backgroundColor = 'lightblue';
  1800. } else if (isFavorite) {
  1801. item.style.backgroundColor = 'lightgreen';
  1802. } else if (isKnown) {
  1803. item.style.backgroundColor = 'yellow';
  1804. } else {
  1805. item.style.backgroundColor = '';
  1806. }
  1807.  
  1808. // Enable hide only if filter toggle is ON
  1809. shouldHide = shouldHide && getFilterEnabled();
  1810. item.style.display = shouldHide ? 'none' : '';
  1811.  
  1812. // 3.6) Save info into array
  1813. itemDataArray.push({
  1814. itemElement: item,
  1815. isWatched,
  1816. isFavorite,
  1817. isKnown,
  1818. actors: itemActors,
  1819. originalIndex: index
  1820. });
  1821. });
  1822.  
  1823. // 4) If we enable priority or actress sorting
  1824. if (sortPriorityEnabled || sortActorEnabled) {
  1825. itemDataArray.sort((a, b) => {
  1826. // 4.1) Priority sorting: watched > favorite > known
  1827. if (sortPriorityEnabled) {
  1828. if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1;
  1829. if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1;
  1830. if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1;
  1831. }
  1832. // 4.2) Actress sorting
  1833. if (sortActorEnabled) {
  1834. let aActors = a.actors.join(', ') || '\uFFFF';
  1835. let bActors = b.actors.join(', ') || '\uFFFF';
  1836. if (aActors < bActors) return -1;
  1837. if (aActors > bActors) return 1;
  1838. }
  1839. // 4.3) If none of the above, follow original DOM order
  1840. return a.originalIndex - b.originalIndex;
  1841. });
  1842. }
  1843.  
  1844. // 5) After sorting, re-append itemElements to container
  1845. container.innerHTML = '';
  1846. itemDataArray.forEach(itemData => {
  1847. container.appendChild(itemData.itemElement);
  1848. });
  1849. }
  1850.  
  1851. // ===== 13. Floating buttons =====
  1852. function createFloatingButtons() {
  1853. let filterButton = document.createElement('button');
  1854. filterButton.id = 'filterButton';
  1855. filterButton.textContent = '屏蔽';
  1856. filterButton.classList.add('floating-button');
  1857. document.body.appendChild(filterButton);
  1858.  
  1859. let favoriteButton = document.createElement('button');
  1860. favoriteButton.id = 'favoriteButton';
  1861. favoriteButton.textContent = '喜爱';
  1862. favoriteButton.classList.add('floating-button');
  1863. document.body.appendChild(favoriteButton);
  1864.  
  1865. let knownButton = document.createElement('button');
  1866. knownButton.id = 'knownButton';
  1867. knownButton.textContent = '认识';
  1868. knownButton.classList.add('floating-button');
  1869. document.body.appendChild(knownButton);
  1870.  
  1871. let watchedButton = document.createElement('button');
  1872. watchedButton.id = 'watchedButton';
  1873. watchedButton.textContent = '看过';
  1874. watchedButton.classList.add('floating-button');
  1875. document.body.appendChild(watchedButton);
  1876.  
  1877. document.addEventListener('mousedown', function () {
  1878. isSelectingText = true;
  1879. });
  1880. document.addEventListener('mouseup', function (event) {
  1881. isSelectingText = false;
  1882. setTimeout(function () {
  1883. let selectedText = window.getSelection().toString().trim();
  1884. if (selectedText.length > 0) {
  1885. let mouseX = event.clientX;
  1886. let mouseY = event.clientY;
  1887. let offset = 20;
  1888.  
  1889. filterButton.style.left = mouseX + 'px';
  1890. filterButton.style.top = (mouseY + offset) + 'px';
  1891. filterButton.style.display = 'block';
  1892.  
  1893. favoriteButton.style.left = (mouseX + 60) + 'px';
  1894. favoriteButton.style.top = (mouseY + offset) + 'px';
  1895. favoriteButton.style.display = 'block';
  1896.  
  1897. knownButton.style.left = (mouseX + 120) + 'px';
  1898. knownButton.style.top = (mouseY + offset) + 'px';
  1899. knownButton.style.display = 'block';
  1900.  
  1901. watchedButton.style.left = (mouseX + 180) + 'px';
  1902. watchedButton.style.top = (mouseY + offset) + 'px';
  1903. watchedButton.style.display = 'block';
  1904.  
  1905. filterButton.onclick = function (e) {
  1906. e.stopPropagation();
  1907. addKeyword(selectedText, STORAGE_KEY, 'filter');
  1908. hideFloatingButtons();
  1909. };
  1910. favoriteButton.onclick = function (e) {
  1911. e.stopPropagation();
  1912. addKeyword(selectedText, FAVORITES_KEY, 'favorite');
  1913. hideFloatingButtons();
  1914. };
  1915. knownButton.onclick = function (e) {
  1916. e.stopPropagation();
  1917. addKeyword(selectedText, KNOWN_KEY, 'known');
  1918. hideFloatingButtons();
  1919. };
  1920. watchedButton.onclick = function (e) {
  1921. e.stopPropagation();
  1922. addKeyword(selectedText, WATCHED_KEY, 'watched');
  1923. hideFloatingButtons();
  1924. };
  1925. } else {
  1926. hideFloatingButtons();
  1927. }
  1928. }, 0);
  1929. });
  1930.  
  1931. function hideFloatingButtons() {
  1932. filterButton.style.display = 'none';
  1933. favoriteButton.style.display = 'none';
  1934. knownButton.style.display = 'none';
  1935. watchedButton.style.display = 'none';
  1936. }
  1937. }
  1938. createFloatingButtons();
  1939.  
  1940. // ===== 14. modifyPage function =====
  1941. function setupExecuteButtonListener() {
  1942. let executeButton = document.getElementById('execute');
  1943. if (executeButton) {
  1944. executeButton.addEventListener('click', function (event) {
  1945. event.stopPropagation();
  1946. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
  1947. saveSQL();
  1948. modifyPage();
  1949. });
  1950. }
  1951. }
  1952. function setupKeyboardShortcuts() {
  1953. document.addEventListener('keydown', function (event) {
  1954. if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
  1955. event.preventDefault();
  1956. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
  1957. saveSQL();
  1958. modifyPage();
  1959. }
  1960. });
  1961. }
  1962.  
  1963. // ===== 15. init function =====
  1964. function initJinjier() {
  1965. console.log('Initializing jinjier.art...');
  1966. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
  1967. setupExecuteButtonListener();
  1968. setupKeyboardShortcuts();
  1969. setTimeout(() => {
  1970. let editor = document.querySelector('.CodeMirror')?.CodeMirror;
  1971. if (editor) {
  1972. let lastSQL = loadSQL();
  1973. if (lastSQL) {
  1974. editor.setValue(lastSQL);
  1975. }
  1976. let executeButton = document.getElementById('execute');
  1977. if (executeButton) {
  1978. // Automatically click once to execute SQL
  1979. executeButton.click();
  1980. }
  1981. }
  1982. }, 1000);
  1983. }
  1984.  
  1985. function initJavdb() {
  1986. console.log('Initializing javdb.com/rankings...');
  1987. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
  1988. setTimeout(() => {
  1989. modifyPage();
  1990. }, 500);
  1991. }
  1992.  
  1993. function initJavLibrary() {
  1994. console.log('Initializing javlibrary.com...');
  1995.  
  1996. const style = document.createElement('style');
  1997. style.innerHTML = `
  1998. .video {
  1999. height: auto !important;
  2000. max-height: 380px !important;
  2001. overflow-y: auto !important;
  2002. }
  2003. `;
  2004. document.head.appendChild(style);
  2005. GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
  2006. setTimeout(() => {
  2007. modifyPage();
  2008. }, 500);
  2009. }
  2010.  
  2011. // can be used to listen to other event sent by other scripts
  2012. // useful for interation
  2013. // here we have another script which give waterfull like infite scroll and sent a infiniteScoroll:newPageLoaded event, we insert actors and modifypage everytime after newPageLoaded
  2014. function setupEventListener() {
  2015. window.addEventListener('infiniteScroll:newPageLoaded', function(event) {
  2016. console.log('Received infiniteScroll:newPageLoaded event:', event.detail);
  2017. toggleActorsColumn();
  2018. toggleActorsColumn();
  2019. modifyPage();
  2020. });
  2021. }
  2022.  
  2023. function init() {
  2024. createKeywordManager();
  2025. const hostname = window.location.hostname;
  2026. const pathname = window.location.pathname;
  2027. if (isJinjierArt()) {
  2028. initJinjier();
  2029. } else if (isJavdbRankings()) {
  2030. initJavdb();
  2031. } else if (isJavLibrary()) {
  2032. initJavLibrary();
  2033. } else {
  2034. console.log('Detected other site, applying minimal logic...');
  2035. }
  2036.  
  2037. setupEventListener();
  2038. }
  2039. init();
  2040. })();