- // ==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();
- })();