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