您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键整合和过滤多Jav网站的观影记录与偏好,屏蔽你不想看的,高亮你喜欢的演员,让你在最短时间内找到真正想看的内容
// ==UserScript== // @name JAV Manager // @namespace https://example.com // @version 1.0 // @description 一键整合和过滤多Jav网站的观影记录与偏好,屏蔽你不想看的,高亮你喜欢的演员,让你在最短时间内找到真正想看的内容 // @match https://jinjier.art/sql* // @match https://javdb.com/* // @exclude https://javdb.com/actors/* // @exclude https://javdb.com/v/* // @exclude https://javdb.com/search?q=* // @match https://www.javlibrary.com/*/vl_bestrated.php* // @match https://www.javlibrary.com/*/vl_mostwanted.php* // @match https://www.javlibrary.com/*/vl_update.php* // @match https://www.javlibrary.com/*/vl_genre.php* // @match https://www.javlibrary.com/*/vl_newrelease.php* // @match https://www.javlibrary.com/*/vl_newentries.php* // @match https://www.javlibrary.com/*/ // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @connect javbus.com // @connect jable.tv // @connect javmenu.com // @connect javdb.com // @license MIT // ==/UserScript== (function () { 'use strict'; /******************************* * Block 1 * 1. Constant definitions * 2. Utility functions * 3. Style injection * 4. Create section function * 5. Keyword management panel (UI skeleton) *******************************/ // ===== 1. Constant definitions ===== const STORAGE_KEY = 'sql_filter_keywords'; // 屏蔽 const FAVORITES_KEY = 'sql_favorite_keywords'; // 喜爱 const KNOWN_KEY = 'sql_known_keywords'; // 认识 const WATCHED_KEY = 'sql_watched_keywords'; // 看过 const SQL_STORAGE_KEY = 'sql_last_executed'; // 上次执行的SQL const FILTER_TOGGLE_KEY = 'sql_filter_enabled'; // 屏蔽功能开关 const SORT_PRIORITY_KEY = 'sql_sort_priority_enabled'; // 按照priority排序开关 const SORT_ACTOR_KEY = 'sql_sort_actor_enabled'; // 按照演员信息排序开关 const NUMBER_ACTOR_STORAGE_KEY = 'sql_number_actor_info'; // 番号与演员信息存储键名 const ACTORS_COLUMN_SHOULD_INSERT = 'actors_column_should_insert'; const ACTORS_COLUMN_INSERTED_KEY = 'actors_column_inserted'; // 存储演员列是否已插入 let isSelectingText = false; // Mark if the user is currently selecting text // ===== 2. Utility functions ===== // Remove brackets and their contents from actress name function stripParentheses(text) { return text.replace(/\s*([^)]*)/g, '').replace(/\s*\([^)]*\)/g, '').trim(); } // General async GM_xmlhttpRequest function gmRequest(details) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...details, onload: function (response) { resolve(response); }, onerror: function (error) { reject(error); } }); }); } // ===== 3. Style injection ===== function addCustomStyles() { const style = document.createElement('style'); style.innerHTML = ` html { width: 100% !important; margin: 0 !important; } @media screen and (min-width:800px) { html { width: 100% !important; margin: 0 !important; } body { border: none !important; padding: 0 20px !important; box-shadow: none !important; border-radius: 0 !important; } } /* Management panel style */ #keywordManager { max-height: 50px; transition: max-height 0.3s ease-out; overflow: hidden; position: fixed; top: 20px; right: 20px; background-color: #ffffff; border: 1px solid #ccc; padding: 20px; z-index: 1000; width: 400px; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); border-radius: 8px; font-family: Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #333; } #keywordManager.expanded { max-height: 800px; /* Large enough to fit content */ } /* Progress bar style */ #progressOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 3000; } #progressBarContainer { width: 80%; background: #fff; padding: 20px; border-radius: 8px; text-align: center; } #progressBar { width: 100%; background: #e0e0e0; border-radius: 4px; overflow: hidden; margin-top: 10px; } #progressBar div { height: 20px; width: 0; background: #28a745; transition: width 0.3s; } /* Floating button style */ .floating-button { position: fixed; display: none; z-index: 1000; padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; } #filterButton { background-color: #dc3545; color: #fff; } #filterButton:hover { background-color: #c82333; } #favoriteButton { background-color: #28a745; color: #fff; } #favoriteButton:hover { background-color: #218838; } #knownButton { background-color: #17a2b8; color: #fff; } #knownButton:hover { background-color: #117a8b; } #watchedButton { background-color: #007bff; color: #fff; } #watchedButton:hover { background-color: #0056b3; } /* Settings button style */ #settingsButton { padding: 5px 10px; background-color: #ffc107; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; position: absolute; bottom: 20px; right: 20px; } #settingsButton:hover { background-color: #e0a800; } /* Settings modal style */ #settingsModal { position: fixed; /* changed to fixed positioning */ background-color: #fff; padding: 20px; border: 1px solid #ccc; border-radius: 8px; z-index: 4000; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); display: none; width: 250px; max-width: 90vw; max-height: 90vh; } #settingsModal h4 { margin-top: 0; } #settingsModal label { display: block; margin-bottom: 10px; } #settingsModal button.close-settings { padding: 5px 10px; margin-top: 10px; background-color: #dc3545; color: #fff; border: none; border-radius: 4px; cursor: pointer; } #settingsModal button.close-settings:hover { background-color: #c82333; } `; document.head.appendChild(style); } addCustomStyles(); // Inject styles immediately // ===== 4. Create section function ===== function createSection(sectionId, sectionTitle, storageKey) { let section = document.createElement('div'); section.id = sectionId; let title = document.createElement('h4'); title.textContent = sectionTitle; title.style.marginTop = '10px'; title.style.marginBottom = '5px'; title.style.color = '#444'; section.appendChild(title); let inputContainer = document.createElement('div'); inputContainer.style.display = 'flex'; inputContainer.style.marginBottom = '10px'; let input = document.createElement('input'); input.type = 'text'; input.placeholder = '添加关键词'; input.style.flex = '1'; input.style.padding = '5px'; input.style.border = '1px solid #ccc'; input.style.borderRadius = '4px'; let addButton = document.createElement('button'); addButton.textContent = '添加'; addButton.style.padding = '5px 10px'; addButton.style.marginLeft = '5px'; addButton.style.backgroundColor = '#28a745'; addButton.style.color = '#fff'; addButton.style.border = 'none'; addButton.style.borderRadius = '4px'; addButton.style.cursor = 'pointer'; addButton.onmouseover = function () { addButton.style.backgroundColor = '#218838'; }; addButton.onmouseout = function () { addButton.style.backgroundColor = '#28a745'; }; addButton.onclick = function (event) { event.stopPropagation(); // Prevent triggering panel toggle let keyword = input.value.trim(); if (keyword) { addKeyword(keyword, storageKey, sectionId); input.value = ''; } }; inputContainer.appendChild(input); inputContainer.appendChild(addButton); section.appendChild(inputContainer); // Keyword list let list = document.createElement('ul'); list.style.listStyleType = 'none'; list.style.padding = '0'; list.style.maxHeight = '200px'; list.style.overflowY = 'auto'; section.appendChild(list); // Bottom container, containing batch import button and feature toggle let bottomContainer = document.createElement('div'); bottomContainer.style.display = 'flex'; bottomContainer.style.alignItems = 'center'; // Batch import button let batchImportButton = document.createElement('button'); batchImportButton.textContent = '批量导入'; batchImportButton.style.padding = '5px 10px'; batchImportButton.style.backgroundColor = '#17a2b8'; batchImportButton.style.color = '#fff'; batchImportButton.style.border = 'none'; batchImportButton.style.borderRadius = '4px'; batchImportButton.style.cursor = 'pointer'; batchImportButton.style.marginRight = '10px'; batchImportButton.onmouseover = function () { batchImportButton.style.backgroundColor = '#117a8b'; }; batchImportButton.onmouseout = function () { batchImportButton.style.backgroundColor = '#17a2b8'; }; batchImportButton.onclick = function (event) { event.stopPropagation(); // Prevent triggering panel toggle showBatchImportModal(storageKey, sectionId); }; bottomContainer.appendChild(batchImportButton); // If it is the "屏蔽" section, add an enable filter toggle if (storageKey === STORAGE_KEY) { let filterToggleContainer = document.createElement('div'); filterToggleContainer.style.display = 'flex'; filterToggleContainer.style.alignItems = 'center'; filterToggleContainer.style.marginLeft = '10px'; let filterToggleSwitch = document.createElement('input'); filterToggleSwitch.type = 'checkbox'; filterToggleSwitch.checked = getFilterEnabled(); filterToggleSwitch.onchange = function () { GM_setValue(FILTER_TOGGLE_KEY, filterToggleSwitch.checked); modifyPage(); // Re-process the page }; filterToggleContainer.appendChild(filterToggleSwitch); let filterToggleLabel = document.createElement('label'); filterToggleLabel.textContent = ' 启用屏蔽'; filterToggleLabel.style.marginLeft = '5px'; filterToggleContainer.appendChild(filterToggleLabel); bottomContainer.appendChild(filterToggleContainer); } section.appendChild(bottomContainer); return section; } // ===== 5. Keyword management panel (UI skeleton) ===== function createKeywordManager() { let manager = document.createElement('div'); manager.id = 'keywordManager'; // Title bar let title = document.createElement('div'); title.style.display = 'flex'; title.style.justifyContent = 'space-between'; title.style.alignItems = 'center'; title.style.cursor = 'pointer'; let titleText = document.createElement('h3'); titleText.textContent = '管理面板'; titleText.style.fontSize = '16px'; titleText.style.color = '#444'; titleText.style.margin = '0'; let titleRightContainer = document.createElement('div'); titleRightContainer.style.display = 'flex'; titleRightContainer.style.alignItems = 'center'; // Export button let exportButton = document.createElement('button'); exportButton.textContent = '导出关键词'; exportButton.id = 'exportKeywordsButton'; exportButton.style.padding = '5px 10px'; exportButton.style.backgroundColor = '#007bff'; exportButton.style.color = '#fff'; exportButton.style.border = 'none'; exportButton.style.borderRadius = '4px'; exportButton.style.cursor = 'pointer'; exportButton.style.fontSize = '12px'; exportButton.style.marginRight = '10px'; exportButton.onmouseover = function () { exportButton.style.backgroundColor = '#0056b3'; }; exportButton.onmouseout = function () { exportButton.style.backgroundColor = '#007bff'; }; exportButton.onclick = function (event) { event.stopPropagation(); exportKeywordsToCSV(); }; titleRightContainer.appendChild(exportButton); // Load/hide actress info button let loadActorsButton = document.createElement('button'); loadActorsButton.textContent = '加载演员信息'; loadActorsButton.id = 'loadActorsButton'; loadActorsButton.style.padding = '5px 10px'; loadActorsButton.style.backgroundColor = '#6c757d'; loadActorsButton.style.color = '#fff'; loadActorsButton.style.border = 'none'; loadActorsButton.style.borderRadius = '4px'; loadActorsButton.style.cursor = 'pointer'; loadActorsButton.style.fontSize = '12px'; loadActorsButton.style.marginLeft = '0px'; loadActorsButton.onmouseover = function () { loadActorsButton.style.backgroundColor = '#5a6268'; }; loadActorsButton.onmouseout = function () { loadActorsButton.style.backgroundColor = '#6c757d'; }; loadActorsButton.onclick = function (event) { event.stopPropagation(); toggleActorsColumn(); }; titleRightContainer.appendChild(loadActorsButton); let toggleIcon = document.createElement('span'); toggleIcon.textContent = '▼'; toggleIcon.style.fontSize = '18px'; toggleIcon.style.marginLeft = '10px'; titleRightContainer.appendChild(toggleIcon); title.appendChild(titleText); title.appendChild(titleRightContainer); title.onclick = toggleManager; manager.appendChild(title); // Navigation bar let nav = document.createElement('div'); nav.style.display = 'flex'; nav.style.marginTop = '10px'; // Create navigation buttons let favoriteTab = createNavButton('喜爱', 'favorite'); let knownTab = createNavButton('认识', 'known'); let filterTab = createNavButton('屏蔽', 'filter'); let watchedTab = createNavButton('看过', 'watched'); // Newly added nav.appendChild(favoriteTab); nav.appendChild(knownTab); nav.appendChild(filterTab); nav.appendChild(watchedTab); manager.appendChild(nav); // Create 4 sections let favoriteSection = createSection('favorite', '喜爱', FAVORITES_KEY); let knownSection = createSection('known', '认识', KNOWN_KEY); let filterSection = createSection('filter', '屏蔽', STORAGE_KEY); let watchedSection = createSection('watched', '看过', WATCHED_KEY); // Initially hide all except "favorite" knownSection.style.display = 'none'; filterSection.style.display = 'none'; watchedSection.style.display = 'none'; manager.appendChild(favoriteSection); manager.appendChild(knownSection); manager.appendChild(filterSection); manager.appendChild(watchedSection); // Settings button let settingsButton = document.createElement('button'); settingsButton.id = 'settingsButton'; settingsButton.textContent = '设置'; settingsButton.style.display = 'none'; // Default hidden manager.appendChild(settingsButton); // Create settings modal let settingsModal = document.createElement('div'); settingsModal.id = 'settingsModal'; let settingsTitle = document.createElement('h4'); settingsTitle.textContent = '排序设置'; settingsModal.appendChild(settingsTitle); // Sort by priority let priorityLabel = document.createElement('label'); let priorityCheckbox = document.createElement('input'); priorityCheckbox.type = 'checkbox'; priorityCheckbox.checked = getSortPriorityEnabled(); priorityCheckbox.onchange = function () { GM_setValue(SORT_PRIORITY_KEY, priorityCheckbox.checked); modifyPage(); }; priorityLabel.appendChild(priorityCheckbox); priorityLabel.appendChild(document.createTextNode(' 按照喜爱/认识/看过排序')); settingsModal.appendChild(priorityLabel); // Sort by actress info let actorSortLabel = document.createElement('label'); let actorSortCheckbox = document.createElement('input'); actorSortCheckbox.type = 'checkbox'; actorSortCheckbox.checked = getSortActorEnabled(); actorSortCheckbox.onchange = function () { GM_setValue(SORT_ACTOR_KEY, actorSortCheckbox.checked); if (actorSortCheckbox.checked) { // Automatically load actress info GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, true); } modifyPage(); }; actorSortLabel.appendChild(actorSortCheckbox); actorSortLabel.appendChild(document.createTextNode(' 按照演员信息排序')); settingsModal.appendChild(actorSortLabel); // Close button let closeButton = document.createElement('button'); closeButton.textContent = '关闭'; closeButton.classList.add('close-settings'); closeButton.onclick = function () { settingsModal.style.display = 'none'; }; settingsModal.appendChild(closeButton); document.body.appendChild(settingsModal); settingsButton.onclick = function (event) { event.stopPropagation(); // Calculate modal position const rect = settingsButton.getBoundingClientRect(); const modalWidth = 250; const modalHeight = settingsModal.offsetHeight || 200; let top = rect.bottom + 5; let left = rect.left; if (left + modalWidth > window.innerWidth) { left = window.innerWidth - modalWidth - 10; } if (top + modalHeight > window.innerHeight) { top = rect.top - modalHeight - 5; } if (left < 10) left = 10; if (top < 10) top = 10; settingsModal.style.top = `${top}px`; settingsModal.style.left = `${left}px`; settingsModal.style.display = 'block'; }; // Click outside the modal to close window.onclick = function (event) { if (event.target === settingsModal) { settingsModal.style.display = 'none'; } }; document.body.appendChild(manager); // Initialize list content for each section updateKeywordList(FAVORITES_KEY, 'favorite'); updateKeywordList(KNOWN_KEY, 'known'); updateKeywordList(STORAGE_KEY, 'filter'); updateKeywordList(WATCHED_KEY, 'watched'); // Panel expand/collapse function toggleManager() { if (manager.classList.contains('expanded')) { manager.classList.remove('expanded'); toggleIcon.textContent = '▼'; settingsButton.style.display = 'none'; } else { manager.classList.add('expanded'); toggleIcon.textContent = '▲'; settingsButton.style.display = 'block'; } } function createNavButton(text, sectionId) { let button = document.createElement('button'); button.textContent = text; button.style.flex = '1'; button.style.padding = '5px'; button.style.border = 'none'; button.style.backgroundColor = '#f8f9fa'; button.style.color = '#007bff'; button.style.cursor = 'pointer'; button.style.borderRadius = '4px'; button.style.marginRight = '5px'; button.onclick = function () { switchSection(sectionId); }; return button; } function switchSection(section) { let tabs = { favorite: favoriteTab, known: knownTab, filter: filterTab, watched: watchedTab }; let sections = { favorite: favoriteSection, known: knownSection, filter: filterSection, watched: watchedSection }; for (let key in tabs) { if (key === section) { tabs[key].style.backgroundColor = '#007bff'; tabs[key].style.color = '#fff'; sections[key].style.display = 'block'; } else { tabs[key].style.backgroundColor = '#f8f9fa'; tabs[key].style.color = '#007bff'; sections[key].style.display = 'none'; } } } } /******************************* * Block 2 * 6. Keyword management functions * 7. SQL handling functions * 10. Batch import and export functions *******************************/ // ===== 6. Keyword management functions ===== function getStoredKeywords(storageKey) { return GM_getValue(storageKey, []); } function getFilterEnabled() { return GM_getValue(FILTER_TOGGLE_KEY, false); } function getSortPriorityEnabled() { return GM_getValue(SORT_PRIORITY_KEY, false); } function getSortActorEnabled() { return GM_getValue(SORT_ACTOR_KEY, false); } function saveKeywords(keywords, storageKey, sectionId) { GM_setValue(storageKey, keywords); updateKeywordList(storageKey, sectionId); modifyPage(); // Update highlight } function addKeyword(keyword, storageKey, sectionId) { let keywords = getStoredKeywords(storageKey); if (!keywords.includes(keyword)) { // Ensure exclusivity across lists if (storageKey === FAVORITES_KEY) { removeKeywordByValue(keyword, KNOWN_KEY, 'known'); removeKeywordByValue(keyword, STORAGE_KEY, 'filter'); removeKeywordByValue(keyword, WATCHED_KEY, 'watched'); } else if (storageKey === KNOWN_KEY) { removeKeywordByValue(keyword, STORAGE_KEY, 'filter'); removeKeywordByValue(keyword, WATCHED_KEY, 'watched'); } else if (storageKey === WATCHED_KEY) { removeKeywordByValue(keyword, FAVORITES_KEY, 'favorite'); removeKeywordByValue(keyword, KNOWN_KEY, 'known'); removeKeywordByValue(keyword, STORAGE_KEY, 'filter'); } keywords.push(keyword); saveKeywords(keywords, storageKey, sectionId); } else { alert('关键词已存在'); } } function removeKeyword(index, storageKey, sectionId) { let keywords = getStoredKeywords(storageKey); keywords.splice(index, 1); saveKeywords(keywords, storageKey, sectionId); } function removeKeywordByValue(keyword, storageKey, sectionId) { let keywords = getStoredKeywords(storageKey); let idx = keywords.indexOf(keyword); if (idx !== -1) { keywords.splice(idx, 1); saveKeywords(keywords, storageKey, sectionId); } } function updateKeywordList(storageKey, sectionId) { let keywords = getStoredKeywords(storageKey); let section = document.getElementById(sectionId); if (!section) return; let list = section.querySelector('ul'); list.innerHTML = ''; keywords.forEach((keyword, index) => { let listItem = document.createElement('li'); listItem.style.display = 'flex'; listItem.style.justifyContent = 'space-between'; listItem.style.alignItems = 'center'; listItem.style.padding = '5px 0'; let keywordText = document.createElement('span'); keywordText.textContent = keyword; let buttonContainer = document.createElement('div'); let editButton = document.createElement('button'); editButton.textContent = '编辑'; editButton.style.marginRight = '5px'; editButton.style.padding = '2px 5px'; editButton.style.backgroundColor = '#ffc107'; editButton.style.color = '#fff'; editButton.style.border = 'none'; editButton.style.borderRadius = '4px'; editButton.style.cursor = 'pointer'; editButton.style.fontSize = '12px'; editButton.onmouseover = function () { editButton.style.backgroundColor = '#e0a800'; }; editButton.onmouseout = function () { editButton.style.backgroundColor = '#ffc107'; }; editButton.onclick = function (event) { event.stopPropagation(); showEditInput(index, keyword, storageKey, sectionId); }; let deleteButton = document.createElement('button'); deleteButton.textContent = '删除'; deleteButton.style.padding = '2px 5px'; deleteButton.style.backgroundColor = '#dc3545'; deleteButton.style.color = '#fff'; deleteButton.style.border = 'none'; deleteButton.style.borderRadius = '4px'; deleteButton.style.cursor = 'pointer'; deleteButton.style.fontSize = '12px'; deleteButton.onmouseover = function () { deleteButton.style.backgroundColor = '#c82333'; }; deleteButton.onmouseout = function () { deleteButton.style.backgroundColor = '#dc3545'; }; deleteButton.onclick = function (event) { event.stopPropagation(); removeKeyword(index, storageKey, sectionId); }; buttonContainer.appendChild(editButton); buttonContainer.appendChild(deleteButton); listItem.appendChild(keywordText); listItem.appendChild(buttonContainer); list.appendChild(listItem); }); } function showEditInput(index, oldKeyword, storageKey, sectionId) { let newKeyword = prompt('编辑关键词', oldKeyword); if (newKeyword !== null && newKeyword.trim() !== '') { editKeyword(index, newKeyword.trim(), storageKey, sectionId); } } function editKeyword(index, newKeyword, storageKey, sectionId) { let keywords = getStoredKeywords(storageKey); if (!keywords.includes(newKeyword)) { // Ensure uniqueness if (storageKey === FAVORITES_KEY) { removeKeywordByValue(newKeyword, KNOWN_KEY, 'known'); removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter'); removeKeywordByValue(newKeyword, WATCHED_KEY, 'watched'); } else if (storageKey === KNOWN_KEY) { removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter'); removeKeywordByValue(newKeyword, WATCHED_KEY, 'watched'); } else if (storageKey === WATCHED_KEY) { removeKeywordByValue(newKeyword, FAVORITES_KEY, 'favorite'); removeKeywordByValue(newKeyword, KNOWN_KEY, 'known'); removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter'); } keywords[index] = newKeyword; saveKeywords(keywords, storageKey, sectionId); } else { alert('关键词已存在'); } } // ===== 7. SQL handling functions ===== function saveSQL() { const editor = document.querySelector('.CodeMirror')?.CodeMirror; if (editor) { const sql = editor.getValue(); // Get SQL content from CodeMirror editor GM_setValue(SQL_STORAGE_KEY, sql); // Save SQL to the specified storage key console.log('SQL 已保存:', sql); } else { console.error('未找到 CodeMirror 编辑器'); } } function loadSQL() { return GM_getValue(SQL_STORAGE_KEY, ''); } // ===== 10. Batch import and export functions ===== function showBatchImportModal(storageKey, sectionId) { let modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.top = '50%'; modal.style.left = '50%'; modal.style.transform = 'translate(-50%, -50%)'; modal.style.backgroundColor = '#fff'; modal.style.padding = '20px'; modal.style.border = '1px solid #ccc'; modal.style.borderRadius = '8px'; modal.style.zIndex = '2000'; modal.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.1)'; let textarea = document.createElement('textarea'); textarea.placeholder = '请输入要导入的关键词,使用空格或换行分隔'; textarea.style.width = '300px'; textarea.style.height = '150px'; textarea.style.marginBottom = '10px'; modal.appendChild(textarea); let buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.justifyContent = 'space-between'; let importButton = document.createElement('button'); importButton.textContent = '导入'; importButton.style.padding = '5px 10px'; importButton.style.backgroundColor = '#28a745'; importButton.style.color = '#fff'; importButton.style.border = 'none'; importButton.style.borderRadius = '4px'; importButton.style.cursor = 'pointer'; importButton.onmouseover = function () { importButton.style.backgroundColor = '#218838'; }; importButton.onmouseout = function () { importButton.style.backgroundColor = '#28a745'; }; importButton.onclick = function (event) { event.stopPropagation(); let inputText = textarea.value.trim(); if (inputText) { let newKeywords = inputText.split(/\s+|\n+/); let keywords = getStoredKeywords(storageKey); let duplicates = []; newKeywords.forEach(keyword => { keyword = keyword.trim(); if (keyword) { if (!keywords.includes(keyword)) { if (storageKey === FAVORITES_KEY) { removeKeywordByValue(keyword, KNOWN_KEY, 'known'); removeKeywordByValue(keyword, STORAGE_KEY, 'filter'); removeKeywordByValue(keyword, WATCHED_KEY, 'watched'); } else if (storageKey === KNOWN_KEY) { removeKeywordByValue(keyword, STORAGE_KEY, 'filter'); removeKeywordByValue(keyword, WATCHED_KEY, 'watched'); } else if (storageKey === WATCHED_KEY) { removeKeywordByValue(keyword, FAVORITES_KEY, 'favorite'); removeKeywordByValue(keyword, KNOWN_KEY, 'known'); removeKeywordByValue(keyword, STORAGE_KEY, 'filter'); } keywords.push(keyword); } else { duplicates.push(keyword); } } }); saveKeywords(keywords, storageKey, sectionId); if (duplicates.length > 0) { alert(`以下关键词已存在于 "${sectionId}" 列表中:\n${duplicates.join(', ')}`); } } document.body.removeChild(modal); }; buttonContainer.appendChild(importButton); let cancelButton = document.createElement('button'); cancelButton.textContent = '取消'; cancelButton.style.padding = '5px 10px'; cancelButton.style.backgroundColor = '#dc3545'; cancelButton.style.color = '#fff'; cancelButton.style.border = 'none'; cancelButton.style.borderRadius = '4px'; cancelButton.style.cursor = 'pointer'; cancelButton.onmouseover = function () { cancelButton.style.backgroundColor = '#c82333'; }; cancelButton.onmouseout = function () { cancelButton.style.backgroundColor = '#dc3545'; }; cancelButton.onclick = function (event) { event.stopPropagation(); document.body.removeChild(modal); }; buttonContainer.appendChild(cancelButton); modal.appendChild(buttonContainer); document.body.appendChild(modal); } function exportKeywordsToCSV() { let favorites = getStoredKeywords(FAVORITES_KEY); let knowns = getStoredKeywords(KNOWN_KEY); let filters = getStoredKeywords(STORAGE_KEY); let watched = getStoredKeywords(WATCHED_KEY); let maxLength = Math.max(favorites.length, knowns.length, filters.length, watched.length); let bom = '\uFEFF'; let csvContent = bom + '喜爱,认识,看过,屏蔽\n'; for (let i = 0; i < maxLength; i++) { let fav = favorites[i] ? `"${favorites[i]}"` : ''; let kno = knowns[i] ? `"${knowns[i]}"` : ''; let wat = watched[i] ? `"${watched[i]}"` : ''; let fil = filters[i] ? `"${filters[i]}"` : ''; csvContent += `${fav},${kno},${wat},${fil}\n`; } GM_download({ url: 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent), name: 'keywords.csv', saveAs: true }); } /******************************* * Block 3 * 8. Actress info extraction * 9. Actress info crawling * 11. Actress info insertion and hiding *******************************/ // ===== 8. Actress info extraction (parse HTML) ===== async function parseActors(doc, source, code) { let actors = []; if (source === 'javbus.com') { let starElements = doc.querySelectorAll('div.star-box-common li div.star-name a'); starElements.forEach(a => { let actorName = a.textContent.trim(); if (actorName) { actors.push(stripParentheses(actorName)); } }); console.log(`[parseActors] javbus.com: Found actresses:`, actors); return actors; } else if (source === 'javmenu.com') { let actressLinks = doc.querySelectorAll('a.actress'); actressLinks.forEach(link => { let actorName = link.textContent.trim(); if (actorName) { actors.push(stripParentheses(actorName)); } }); console.log(`[parseActors] javmenu.com: Found actresses:`, actors); return actors; } else if (source === 'jable.tv') { let modelsDiv = doc.querySelector('div.models'); if (!modelsDiv) { console.warn(`[parseActors] jable.tv: Missing div.models element, returning empty array`); return actors; } let modelLinks = modelsDiv.querySelectorAll('a.model'); modelLinks.forEach(link => { let span = link.querySelector('span.placeholder.rounded-circle'); if (span) { let actorName = span.getAttribute('data-original-title') || span.getAttribute('title'); if (actorName) { actors.push(stripParentheses(actorName.trim())); } } let img = link.querySelector('img.avatar.rounded-circle'); if (img) { let actorName = img.getAttribute('data-original-title') || img.getAttribute('title'); if (actorName) { actors.push(stripParentheses(actorName.trim())); } } }); console.log(`[parseActors] jable.tv: Found actresses:`, actors); return actors; } else if (source === 'javdb.com') { // 1) Check if code is provided if (!code) { console.warn('[parseActors] javdb.com: Code not provided, cannot parse actress info'); return actors; } // 2) Find the video detail link in search results let codeLink = Array.from(doc.querySelectorAll('a[href^="/v/"]')).find(link => { return link.textContent.trim().toUpperCase().includes(code.toUpperCase()); }); if (!codeLink) { console.warn(`[parseActors] javdb.com: No link /v/ containing ${code} found, returning empty array`); return actors; } let href = codeLink.getAttribute('href'); let fullURL = `https://javdb.com${href}`; console.log(`[parseActors] javdb.com: Found video link => ${fullURL}`); // 3) Request the video detail page try { let response = await gmRequest({ method: 'GET', url: fullURL, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Connection': 'keep-alive', 'Referer': 'https://www.google.com/', 'Upgrade-Insecure-Requests': '1', 'Cache-Control': 'max-age=0' } }); // 4) Check return code if (response.status === 200) { console.log(`[parseActors] javdb.com: Got detail page successfully (status=200) => parse actress info now`); // 5) Parse actress info let parser = new DOMParser(); let detailedDoc = parser.parseFromString(response.responseText, 'text/html'); let panelBlocks = detailedDoc.querySelectorAll('div.panel-block'); // Note: "演員:" in Traditional Chinese let actorDiv = Array.from(panelBlocks).find(block => { let strong = block.querySelector('strong'); return strong && strong.textContent.trim() === '演員:'; }); if (!actorDiv) { console.warn('[parseActors] javdb.com: No <strong>演員:</strong> panel-block found in detail page'); } else { let actorLinks = actorDiv.querySelectorAll('span.value a[href^="/actors/"]'); actorLinks.forEach(link => { let symbol = link.nextElementSibling; // If male actors are needed, remove female check if (symbol && symbol.classList.contains('symbol') && symbol.classList.contains('female')) { let actorName = link.textContent.trim(); if (actorName) { actors.push(stripParentheses(actorName)); } } }); } } else { console.warn(`[parseActors] javdb.com: Failed to get detail page, status=${response.status}, returning empty array`); } } catch (error) { console.error(`[parseActors] javdb.com: Error fetching detail page ${fullURL}:`, error); } console.log(`[parseActors] javdb.com: Final actress list =>`, actors); return actors; } else { // Unknown source return actors; } } // ===== 9. Actress info crawling ===== async function crawlMissingActors(missingActors) { let progressOverlay = document.createElement('div'); progressOverlay.id = 'progressOverlay'; progressOverlay.style.position = 'fixed'; progressOverlay.style.top = '0'; progressOverlay.style.left = '0'; progressOverlay.style.width = '100%'; progressOverlay.style.height = '100%'; progressOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; progressOverlay.style.display = 'flex'; progressOverlay.style.justifyContent = 'center'; progressOverlay.style.alignItems = 'center'; progressOverlay.style.zIndex = '10000'; let progressBarContainer = document.createElement('div'); progressBarContainer.id = 'progressBarContainer'; progressBarContainer.style.width = '50%'; progressBarContainer.style.backgroundColor = '#fff'; progressBarContainer.style.padding = '20px'; progressBarContainer.style.borderRadius = '8px'; progressBarContainer.style.textAlign = 'center'; progressBarContainer.innerHTML = ` <h3>正在爬取演员信息...</h3> <div id="progressBar" style="width: 100%; background-color: #ddd; border-radius: 5px; overflow: hidden; height: 20px; margin-bottom: 10px;"> <div style="width: 0%; height: 100%; background-color: #28a745;"></div> </div> <p id="progressText">0 / ${missingActors.length}</p> `; progressOverlay.appendChild(progressBarContainer); document.body.appendChild(progressOverlay); let progressBar = progressBarContainer.querySelector('#progressBar div'); let progressText = progressBarContainer.querySelector('#progressText'); let total = missingActors.length; let completed = 0; const concurrency = 4; let current = 0; let activeRequests = 0; async function crawl() { while (current < total && activeRequests < concurrency) { fetchActorInfo(missingActors[current]); current++; } if (completed >= total) { document.body.removeChild(progressOverlay); window.location.reload(); } } async function fetchActorInfo(code) { activeRequests++; let primarySources = [ { url: `https://www.javbus.com/${code}`, source: 'javbus.com' }, { url: `https://javdb.com/search?q=${code}`, source: 'javdb.com' }, { url: `https://jable.tv/videos/${code.toLowerCase()}/`, source: 'jable.tv' }, { url: `https://javmenu.com/en/${code}`, source: 'javmenu.com' }, ]; let fetched = false; async function handleResponse(doc, source) { try { let actors = await parseActors(doc, source, code); if (actors.length > 0) { let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {}); numberActorData[code] = actors; GM_setValue(NUMBER_ACTOR_STORAGE_KEY, numberActorData); fetched = true; completeFetch(); } else { await tryNextSource(); } } catch (error) { console.error(`Error parsing actress info for code ${code}:`, error); await tryNextSource(); } } async function tryNextSource() { if (primarySources.length > 0) { let nextSource = primarySources.shift(); let randomDelay = Math.floor(Math.random() * 1000) + 500; await delay(randomDelay); try { let response = await gmRequest({ method: 'GET', url: nextSource.url, headers: { 'User-Agent': getRandomUserAgent(), 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Connection': 'keep-alive', 'Referer': 'https://www.google.com/', 'Upgrade-Insecure-Requests': '1', 'Cache-Control': 'max-age=0' } }); if (response.status === 200) { let parser = new DOMParser(); let doc = parser.parseFromString(response.responseText, 'text/html'); await handleResponse(doc, nextSource.source); } else { await tryNextSource(); } } catch (error) { await tryNextSource(); } } else if (!fetched) { storeEmptyActorInfo(code); completeFetch(); } } function completeFetch() { completed++; activeRequests--; updateProgress(); crawl(); } function storeEmptyActorInfo(code) { let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {}); numberActorData[code] = []; GM_setValue(NUMBER_ACTOR_STORAGE_KEY, numberActorData); } function getRandomUserAgent() { const USER_AGENTS = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', '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', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', ]; return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } await tryNextSource(); } function updateProgress() { progressBar.style.width = `${(completed / total) * 100}%`; progressText.textContent = `${completed} / ${total}`; } crawl(); } // ===== 11. Actress info insertion and hiding ===== function toggleActorsColumn() { let shouldInsertActor = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT, false); if (!shouldInsertActor) { if (!GM_getValue(ACTORS_COLUMN_INSERTED_KEY)) { insertActorsInfo(); GM_setValue(ACTORS_COLUMN_INSERTED_KEY, true); } GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, true); } else { if (GM_getValue(ACTORS_COLUMN_INSERTED_KEY)) { hideActorsInfo(); GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false); } GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, false); } updateLoadActorsButton(); } function updateLoadActorsButton() { let loadActorsButton = document.getElementById('loadActorsButton'); if (!loadActorsButton) return; let shouldInsert = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT); loadActorsButton.textContent = shouldInsert ? '隐藏演员信息' : '加载演员信息'; } function hideActorsInfo() { if (isJinjierArt()) { hideActorsColumnJinjierArt(); } else if (isJavdbRankings()) { hideActorsInfoJavdb(); } else if (isJavLibrary()) { hideActorsInfoJavLibrary(); } } function insertActorsInfo() { if (isJinjierArt()) { loadActorsAndInsertColumnJinjierArt(); } else if (isJavdbRankings()) { loadActorsAndInsertInfoJavdb(); } else if (isJavLibrary()) { loadActorsAndInsertInfoJavLibrary(); } GM_setValue(ACTORS_COLUMN_INSERTED_KEY, true); } function isJinjierArt() { return window.location.hostname.includes('jinjier.art'); } function isJavdbRankings() { return window.location.hostname.includes('javdb.com') && !window.location.pathname.startsWith('/actors/') && !window.location.pathname.startsWith('/v/'); } function isJavLibrary() { // Simple check if domain contains "javlibrary.com" return window.location.hostname.includes('javlibrary.com'); } function hideActorsColumnJinjierArt() { let table = document.querySelector('table'); if (!table) return; let rows = table.querySelectorAll('tr'); rows.forEach(row => { if (row.cells.length > 0) { row.deleteCell(0); } }); } function hideActorsInfoJavdb() { let items = document.querySelectorAll('div.item'); items.forEach(item => { let actorDiv = item.querySelector('.actors-info'); if (actorDiv) actorDiv.remove(); }); } function hideActorsInfoJavLibrary() { let videos = document.querySelectorAll('div.video'); videos.forEach(video => { let actorDiv = video.querySelector('.actors-info'); if (actorDiv) { actorDiv.remove(); } }); } function loadActorsAndInsertColumnJinjierArt() { let codes = new Set(); let links = document.querySelectorAll('a'); links.forEach(link => { let text = link.textContent.trim(); let codeMatch = text.match(/[A-Z]{2,5}-\d{2,5}/i); if (codeMatch) { codes.add(codeMatch[0].toUpperCase()); } }); let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {}); let missingActors = []; codes.forEach(code => { if (!numberActorData[code] || !Array.isArray(numberActorData[code])) { missingActors.push(code); } }); if (missingActors.length > 0) { let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`); if (proceed) { crawlMissingActors(missingActors); } } let codeActorMap = {}; codes.forEach(code => { if (numberActorData[code] && Array.isArray(numberActorData[code])) { codeActorMap[code] = numberActorData[code]; } }); let table = document.querySelector('table'); if (!table) return; let rows = table.querySelectorAll('tr'); rows.forEach((row) => { let codeCell = row.querySelector('td a'); let actorsHTML = ''; if (codeCell) { let codeText = codeCell.textContent.trim().toUpperCase(); let actors = codeActorMap[codeText] || []; 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(''); } let newCell = row.insertCell(0); newCell.innerHTML = actorsHTML; }); } function loadActorsAndInsertInfoJavdb() { let codes = new Set(); let items = document.querySelectorAll('div.item'); items.forEach(item => { let videoTitle = item.querySelector('.video-title'); if (videoTitle) { let strong = videoTitle.querySelector('strong'); if (strong) { let code = strong.textContent.trim().toUpperCase(); let codeMatch = code.match(/[A-Z]{2,5}-\d{2,5}/i); if (codeMatch) { codes.add(codeMatch[0].toUpperCase()); } } } }); let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {}); let missingActors = []; codes.forEach(code => { if (!numberActorData[code] || !Array.isArray(numberActorData[code])) { missingActors.push(code); } }); if (missingActors.length > 0) { let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`); if (proceed) { crawlMissingActors(missingActors); } } let codeActorMap = {}; codes.forEach(code => { if (numberActorData[code] && Array.isArray(numberActorData[code])) { codeActorMap[code] = numberActorData[code]; } }); let insertedItems = 0; items.forEach(item => { let videoTitle = item.querySelector('.video-title'); if (videoTitle) { let strong = videoTitle.querySelector('strong'); if (strong) { let codeText = strong.textContent.trim().toUpperCase(); let actors = codeActorMap[codeText] || []; 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(''); let actorDiv = document.createElement('div'); actorDiv.classList.add('actors-info'); actorDiv.innerHTML = `<strong>演员:</strong> ${actorsHTML}`; actorDiv.style.marginBottom = '5px'; videoTitle.parentNode.insertBefore(actorDiv, videoTitle); insertedItems++; } } }); } function loadActorsAndInsertInfoJavLibrary() { // 1) Collect all codes let codes = new Set(); let videoDivs = document.querySelectorAll('.video'); videoDivs.forEach(video => { let codeElem = video.querySelector('.id'); if (codeElem) { let codeText = codeElem.textContent.trim().toUpperCase(); let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i); if (codeMatch) { codes.add(codeMatch[0].toUpperCase()); } } }); // 2) Check existing number-actor mapping, see if any missing let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {}); let missingActors = []; codes.forEach(code => { if (!numberActorData[code] || !Array.isArray(numberActorData[code])) { missingActors.push(code); } }); if (missingActors.length > 0) { let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`); if (proceed) { crawlMissingActors(missingActors); } } // 3) Construct code => [actors] map let codeActorMap = {}; codes.forEach(code => { if (numberActorData[code] && Array.isArray(numberActorData[code])) { codeActorMap[code] = numberActorData[code]; } }); // 4) Insert actress info into each videoDiv let insertedItems = 0; videoDivs.forEach(video => { let codeElem = video.querySelector('.id'); if (!codeElem) return; let codeText = codeElem.textContent.trim().toUpperCase(); let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i); if (!codeMatch) return; let finalCode = codeMatch[0].toUpperCase(); let actors = codeActorMap[finalCode] || []; if (actors.length > 0) { 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(''); let actorDiv = document.createElement('div'); actorDiv.classList.add('actors-info'); actorDiv.innerHTML = `<strong>演员:</strong> ${actorsHTML}`; actorDiv.style.marginBottom = '5px'; let titleElem = video.querySelector('.title'); if (titleElem) { titleElem.insertAdjacentElement('afterend', actorDiv); insertedItems++; } } }); console.log('[loadActorsAndInsertInfoJavLibrary] Insert actress info successfully:', insertedItems, 'items'); } /******************************* * Block 4 * 12. Highlight and sorting * 13. Floating buttons * 14. modifyPage function * 15. init function *******************************/ // ===== 12. Highlight and sorting ===== function modifyPage() { if (isSelectingText) return; try { let shouldInsertActors = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT, false); let ActorsInserted = GM_getValue(ACTORS_COLUMN_INSERTED_KEY, false); if (shouldInsertActors && !ActorsInserted) { insertActorsInfo(); } updateLoadActorsButton(); let favorites = getStoredKeywords(FAVORITES_KEY); let knowns = getStoredKeywords(KNOWN_KEY); let watched = getStoredKeywords(WATCHED_KEY); let filters = getStoredKeywords(STORAGE_KEY); let sortPriorityEnabled = getSortPriorityEnabled(); let sortActorEnabled = getSortActorEnabled(); if (isJinjierArt()) { highlightRowsJinjierArt(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled); } else if (isJavdbRankings()) { highlightRowsJavdb(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled); } else if (isJavLibrary()) { highlightRowsJavLibrary(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled); } } catch (error) { console.error('Error in modifyPage:', error); } } function highlightRowsJinjierArt(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled) { let rows = Array.from(document.querySelectorAll('table tr')); let rowDataArray = []; rows.forEach((row, rowIndex) => { let cells = row.querySelectorAll('td'); let isFavorite = false; let isKnown = false; let isWatched = false; let actors = []; cells.forEach((cell, cellIndex) => { if (!cell.hasAttribute('data-original-html')) { cell.setAttribute('data-original-html', cell.innerHTML); } else { cell.innerHTML = cell.getAttribute('data-original-html'); } let cellText = cell.textContent; if (favorites.some(k => cellText.includes(k))) { isFavorite = true; } if (knowns.some(k => cellText.includes(k))) { isKnown = true; } if (watched.some(k => cellText.includes(k))) { isWatched = true; } if (cellIndex === 0) { let actorDivs = cell.querySelectorAll('div'); actorDivs.forEach(div => { let actorName = div.textContent.trim(); if (actorName) actors.push(actorName); }); } }); if (isWatched) { row.style.backgroundColor = 'lightblue'; } else if (isFavorite) { row.style.backgroundColor = 'lightgreen'; } else if (isKnown) { row.style.backgroundColor = 'yellow'; } else { row.style.backgroundColor = ''; } let shouldHide = false; cells.forEach(cell => { let cellText = cell.textContent; if (filters.some(k => cellText.includes(k))) { shouldHide = true; } }); shouldHide = shouldHide && getFilterEnabled(); row.style.display = shouldHide ? 'none' : ''; rowDataArray.push({ rowElement: row, isWatched, isFavorite, isKnown, actors, originalIndex: rowIndex }); }); if (sortPriorityEnabled || sortActorEnabled) { rowDataArray.sort((a, b) => { if (sortPriorityEnabled) { if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1; if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1; if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1; } if (sortActorEnabled) { let aActors = a.actors.join(', ') || '\uFFFF'; let bActors = b.actors.join(', ') || '\uFFFF'; if (aActors < bActors) return -1; if (aActors > bActors) return 1; } return a.originalIndex - b.originalIndex; }); } let tbody = document.querySelector('table tbody'); if (tbody) { tbody.innerHTML = ''; rowDataArray.forEach(rowData => { tbody.appendChild(rowData.rowElement); }); } } function highlightRowsJavdb(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled) { let items = Array.from(document.querySelectorAll('div.movie-list.h.cols-4.vcols-8 > div.item')); let itemDataArray = []; items.forEach((item, itemIndex) => { let videoTitleElem = item.querySelector('.video-title'); let isFavorite = false; let isKnown = false; let isWatched = false; let actors = []; if (videoTitleElem) { let textContent = videoTitleElem.textContent; if (favorites.some(k => textContent.includes(k))) { isFavorite = true; } if (knowns.some(k => textContent.includes(k))) { isKnown = true; } if (watched.some(k => textContent.includes(k))) { isWatched = true; } let actorDiv = item.querySelector('.actors-info'); if (actorDiv) { let actorLinks = actorDiv.querySelectorAll('a'); actorLinks.forEach(link => { let actorName = link.textContent.trim(); if (actorName) actors.push(actorName); }); } } let box = item.querySelector('a.box'); if (box) { if (isWatched) { box.style.backgroundColor = 'lightblue'; } else if (isFavorite) { box.style.backgroundColor = 'lightgreen'; } else if (isKnown) { box.style.backgroundColor = 'yellow'; } else { box.style.backgroundColor = ''; } } let shouldHide = false; if (box) { let boxText = box.textContent; if (filters.some(k => boxText.includes(k))) { shouldHide = true; } } shouldHide = shouldHide && getFilterEnabled(); item.style.display = shouldHide ? 'none' : ''; itemDataArray.push({ itemElement: item, isWatched, isFavorite, isKnown, actors, originalIndex: itemIndex }); }); if (sortPriorityEnabled || sortActorEnabled) { itemDataArray.sort((a, b) => { if (sortPriorityEnabled) { if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1; if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1; if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1; } if (sortActorEnabled) { let aActors = a.actors.join(', ') || '\uFFFF'; let bActors = b.actors.join(', ') || '\uFFFF'; if (aActors < bActors) return -1; if (aActors > bActors) return 1; } return a.originalIndex - b.originalIndex; }); } let container = document.querySelector('div.movie-list.h.cols-4.vcols-8'); if (container) { container.innerHTML = ''; itemDataArray.forEach(itemData => { container.appendChild(itemData.itemElement); }); } } function highlightRowsJavLibrary( favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled ) { // 0) Retrieve stored "code -> [actress array]" mapping let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {}); // 1) Find the container that holds all video items (e.g. <div class="videos">) let container = document.querySelector('.videos'); if (!container) { console.warn('[highlightRowsJavLibrary] .videos container not found, exiting'); return; } // 2) Get all video elements, each is <div class="video" id="vid_XXXXX"> let items = Array.from(container.querySelectorAll('div.video')); // 3) Collect item info for potential sorting let itemDataArray = []; items.forEach((item, index) => { // 3.1) Get code and title let codeElem = item.querySelector('.id'); let titleElem = item.querySelector('.title'); let codeText = codeElem ? codeElem.textContent.trim().toUpperCase() : ''; let titleText = titleElem ? titleElem.textContent.trim() : ''; // 3.2) Based on code, retrieve actress array from numberActorData let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i); let codeKey = codeMatch ? codeMatch[0].toUpperCase() : null; let itemActors = (codeKey && Array.isArray(numberActorData[codeKey])) ? numberActorData[codeKey] : []; // 3.3) Determine if this video matches favorites/known/watched by code/title let isFavorite = false; let isKnown = false; let isWatched = false; let shouldHide = false; if (favorites.some(k => codeText.includes(k) || titleText.includes(k))) { isFavorite = true; } if (knowns.some(k => codeText.includes(k) || titleText.includes(k))) { isKnown = true; } if (watched.some(k => codeText.includes(k) || titleText.includes(k))) { isWatched = true; } if (filters.some(k => codeText.includes(k) || titleText.includes(k))) { shouldHide = true; } // 3.4) Check actress info for matches let actorHasFavorite = itemActors.some(actor => favorites.some(k => actor.includes(k)) ); let actorHasKnown = itemActors.some(actor => knowns.some(k => actor.includes(k)) ); let actorHasWatched = itemActors.some(actor => watched.some(k => actor.includes(k)) ); let actorHasFilter = itemActors.some(actor => filters.some(k => actor.includes(k)) ); if (actorHasFavorite) isFavorite = true; if (actorHasKnown) isKnown = true; if (actorHasWatched) isWatched = true; if (actorHasFilter) shouldHide = true; // If the first three are not matched but there's a filter, set true if (!isFavorite && !isKnown && !isWatched && shouldHide) { shouldHide = true; } else { shouldHide = false; } // 3.5) Assign background color: watched > favorite > known if (isWatched) { item.style.backgroundColor = 'lightblue'; } else if (isFavorite) { item.style.backgroundColor = 'lightgreen'; } else if (isKnown) { item.style.backgroundColor = 'yellow'; } else { item.style.backgroundColor = ''; } // Enable hide only if filter toggle is ON shouldHide = shouldHide && getFilterEnabled(); item.style.display = shouldHide ? 'none' : ''; // 3.6) Save info into array itemDataArray.push({ itemElement: item, isWatched, isFavorite, isKnown, actors: itemActors, originalIndex: index }); }); // 4) If we enable priority or actress sorting if (sortPriorityEnabled || sortActorEnabled) { itemDataArray.sort((a, b) => { // 4.1) Priority sorting: watched > favorite > known if (sortPriorityEnabled) { if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1; if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1; if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1; } // 4.2) Actress sorting if (sortActorEnabled) { let aActors = a.actors.join(', ') || '\uFFFF'; let bActors = b.actors.join(', ') || '\uFFFF'; if (aActors < bActors) return -1; if (aActors > bActors) return 1; } // 4.3) If none of the above, follow original DOM order return a.originalIndex - b.originalIndex; }); } // 5) After sorting, re-append itemElements to container container.innerHTML = ''; itemDataArray.forEach(itemData => { container.appendChild(itemData.itemElement); }); } // ===== 13. Floating buttons ===== function createFloatingButtons() { let filterButton = document.createElement('button'); filterButton.id = 'filterButton'; filterButton.textContent = '屏蔽'; filterButton.classList.add('floating-button'); document.body.appendChild(filterButton); let favoriteButton = document.createElement('button'); favoriteButton.id = 'favoriteButton'; favoriteButton.textContent = '喜爱'; favoriteButton.classList.add('floating-button'); document.body.appendChild(favoriteButton); let knownButton = document.createElement('button'); knownButton.id = 'knownButton'; knownButton.textContent = '认识'; knownButton.classList.add('floating-button'); document.body.appendChild(knownButton); let watchedButton = document.createElement('button'); watchedButton.id = 'watchedButton'; watchedButton.textContent = '看过'; watchedButton.classList.add('floating-button'); document.body.appendChild(watchedButton); document.addEventListener('mousedown', function () { isSelectingText = true; }); document.addEventListener('mouseup', function (event) { isSelectingText = false; setTimeout(function () { let selectedText = window.getSelection().toString().trim(); if (selectedText.length > 0) { let mouseX = event.clientX; let mouseY = event.clientY; let offset = 20; filterButton.style.left = mouseX + 'px'; filterButton.style.top = (mouseY + offset) + 'px'; filterButton.style.display = 'block'; favoriteButton.style.left = (mouseX + 60) + 'px'; favoriteButton.style.top = (mouseY + offset) + 'px'; favoriteButton.style.display = 'block'; knownButton.style.left = (mouseX + 120) + 'px'; knownButton.style.top = (mouseY + offset) + 'px'; knownButton.style.display = 'block'; watchedButton.style.left = (mouseX + 180) + 'px'; watchedButton.style.top = (mouseY + offset) + 'px'; watchedButton.style.display = 'block'; filterButton.onclick = function (e) { e.stopPropagation(); addKeyword(selectedText, STORAGE_KEY, 'filter'); hideFloatingButtons(); }; favoriteButton.onclick = function (e) { e.stopPropagation(); addKeyword(selectedText, FAVORITES_KEY, 'favorite'); hideFloatingButtons(); }; knownButton.onclick = function (e) { e.stopPropagation(); addKeyword(selectedText, KNOWN_KEY, 'known'); hideFloatingButtons(); }; watchedButton.onclick = function (e) { e.stopPropagation(); addKeyword(selectedText, WATCHED_KEY, 'watched'); hideFloatingButtons(); }; } else { hideFloatingButtons(); } }, 0); }); function hideFloatingButtons() { filterButton.style.display = 'none'; favoriteButton.style.display = 'none'; knownButton.style.display = 'none'; watchedButton.style.display = 'none'; } } createFloatingButtons(); // ===== 14. modifyPage function ===== function setupExecuteButtonListener() { let executeButton = document.getElementById('execute'); if (executeButton) { executeButton.addEventListener('click', function (event) { event.stopPropagation(); GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false); saveSQL(); modifyPage(); }); } } function setupKeyboardShortcuts() { document.addEventListener('keydown', function (event) { if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false); saveSQL(); modifyPage(); } }); } // ===== 15. init function ===== function initJinjier() { console.log('Initializing jinjier.art...'); GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false); setupExecuteButtonListener(); setupKeyboardShortcuts(); setTimeout(() => { let editor = document.querySelector('.CodeMirror')?.CodeMirror; if (editor) { let lastSQL = loadSQL(); if (lastSQL) { editor.setValue(lastSQL); } let executeButton = document.getElementById('execute'); if (executeButton) { // Automatically click once to execute SQL executeButton.click(); } } }, 1000); } function initJavdb() { console.log('Initializing javdb.com/rankings...'); GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false); setTimeout(() => { modifyPage(); }, 500); } function initJavLibrary() { console.log('Initializing javlibrary.com...'); const style = document.createElement('style'); style.innerHTML = ` .video { height: auto !important; max-height: 380px !important; overflow-y: auto !important; } `; document.head.appendChild(style); GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false); setTimeout(() => { modifyPage(); }, 500); } // can be used to listen to other event sent by other scripts // useful for interation // 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 function setupEventListener() { window.addEventListener('infiniteScroll:newPageLoaded', function(event) { console.log('Received infiniteScroll:newPageLoaded event:', event.detail); toggleActorsColumn(); toggleActorsColumn(); modifyPage(); }); } function init() { createKeywordManager(); const hostname = window.location.hostname; const pathname = window.location.pathname; if (isJinjierArt()) { initJinjier(); } else if (isJavdbRankings()) { initJavdb(); } else if (isJavLibrary()) { initJavLibrary(); } else { console.log('Detected other site, applying minimal logic...'); } setupEventListener(); } init(); })();