// ==UserScript==
// @name ExHentai Lanraragi Checker 1.4
// @namespace https://github.com/Putarku
// @match https://exhentai.org/*
// @match https://e-hentai.org/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @license MIT
// @version 1.4
// @author Putarku
// @description Checks if galleries on ExHentai/E-Hentai are already in your Lanraragi library and marks them by inserting a span at the beginning of the title.
// ==/UserScript==
(function() {
'use strict';
// --- 用户配置开始 ---
const LRR_SERVER_URL = 'http://localhost:3000'; // 替换为您的 Lanraragi 服务器地址
const LRR_API_KEY = ''; // 如果您的 Lanraragi API 需要密钥,请填写
const MAX_CONCURRENT_REQUESTS = 5; // 最大并发请求数,避免服务器过载
// --- 用户配置结束 ---
GM_addStyle(`
.lrr-marker-span {
font-weight: bold;
border-radius: 3px;
padding: 0px 3px;
margin-right: 4px; /* 与 visied.js 的 ● 标记或标题文本的间距 */
font-size: 0.9em;
}
.lrr-marker-downloaded {
color: #28a745; /* 绿色 */
background-color: #49995d;
}
.lrr-marker-file {
color: #356ddc; /* 蓝色 */
background-color: #894ab0;
}
.lrr-marker-error {
color: #dc3545; /* 红色 */
background-color: #fbe9ea;
}
`);
const CACHE_DURATION = 60 * 60 * 1000; // 1h in milliseconds
const CLEANUP_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 7 days cleanup interval
function getCache(key) {
const cached = localStorage.getItem(key);
if (cached) {
const { timestamp, data } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION) {
return data;
}
}
return null;
}
function setCache(key, data) {
const item = {
timestamp: Date.now(),
data: data
};
localStorage.setItem(key, JSON.stringify(item));
}
// 清理过期缓存
function cleanupExpiredCache() {
const lastCleanup = localStorage.getItem('lrr-cache-last-cleanup');
const currentTime = Date.now();
// 如果距离上次清理超过7天,执行清理
if (!lastCleanup || (currentTime - parseInt(lastCleanup)) > CLEANUP_INTERVAL) {
console.log('[LRR Checker] Starting cache cleanup...');
let removedCount = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('lrr-checker-')) {
try {
const item = localStorage.getItem(key);
if (item) {
const cacheData = JSON.parse(item);
if (currentTime - cacheData.timestamp > CACHE_DURATION) {
localStorage.removeItem(key);
removedCount++;
i--; // 因为删除后数组长度变化
}
}
} catch (e) {
console.error(`[LRR Checker] Error cleaning up cache key ${key}:`, e);
}
}
}
localStorage.setItem('lrr-cache-last-cleanup', currentTime.toString());
console.log(`[LRR Checker] Cache cleanup completed. Removed ${removedCount} expired items.`);
}
}
// 将GM_xmlhttpRequest包装为Promise
function makeRequest(options) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: options.method,
url: options.url,
headers: options.headers,
onload: function(response) {
resolve(response);
},
onerror: function(error) {
reject(error);
}
});
});
}
// 限制并发请求数量的函数
async function processInBatches(items, processFn, batchSize) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(processFn);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return results;
}
// 收集需要查询的画廊信息
const galleryLinks = document.querySelectorAll('.itg .gl1t a[href*="/g/"]');
const galleriesToCheck = [];
galleryLinks.forEach(linkElement => {
const galleryUrl = linkElement.href;
const titleElement = linkElement.querySelector('.glink');
if (!galleryUrl || !titleElement) {
return;
}
if (titleElement.querySelector('.lrr-marker-span')) {
return;
}
const cacheKey = `lrr-checker-${galleryUrl}`;
const cachedData = getCache(cacheKey);
if (cachedData) {
console.log(`[LRR Checker] Using cached data for: ${galleryUrl}`);
handleResponse(cachedData, titleElement, galleryUrl);
return;
}
galleriesToCheck.push({
galleryUrl,
titleElement,
cacheKey
});
});
// 处理单个画廊的查询
async function processGallery(gallery) {
const { galleryUrl, titleElement, cacheKey } = gallery;
const apiUrl = `${LRR_SERVER_URL}/api/plugins/use?plugin=urlfinder&arg=${encodeURIComponent(galleryUrl)}`;
const headers = {};
if (LRR_API_KEY) {
headers['Authorization'] = `Bearer ${LRR_API_KEY}`;
}
try {
const response = await makeRequest({
method: 'POST',
url: apiUrl,
headers: headers
});
try {
const result = JSON.parse(response.responseText);
setCache(cacheKey, result);
handleResponse(result, titleElement, galleryUrl);
return { success: true, galleryUrl };
} catch (e) {
console.error(`[LRR Checker] Error parsing JSON for ${galleryUrl}:`, e, response.responseText);
let markerSpan = document.createElement('span');
markerSpan.classList.add('lrr-marker-span', 'lrr-marker-error');
markerSpan.textContent = '(LRR ❓)';
if (titleElement) titleElement.prepend(markerSpan);
return { success: false, galleryUrl, error: e };
}
} catch (error) {
console.error(`[LRR Checker] Network error checking ${galleryUrl}:`, error);
let markerSpan = document.createElement('span');
markerSpan.classList.add('lrr-marker-span', 'lrr-marker-error');
markerSpan.textContent = '(LRR ❓)';
if (titleElement) titleElement.prepend(markerSpan);
return { success: false, galleryUrl, error };
}
}
// 执行缓存清理
cleanupExpiredCache();
// 并行处理所有画廊查询,限制并发数
if (galleriesToCheck.length > 0) {
console.log(`[LRR Checker] Processing ${galleriesToCheck.length} galleries in parallel batches`);
processInBatches(galleriesToCheck, processGallery, MAX_CONCURRENT_REQUESTS)
.then(results => {
console.log(`[LRR Checker] Completed all gallery checks. Success: ${results.filter(r => r.success).length}, Failed: ${results.filter(r => !r.success).length}`);
})
.catch(error => {
console.error(`[LRR Checker] Error in batch processing:`, error);
});
}
// 将备用搜索也改为Promise方式
async function performAlternativeSearch(searchQuery, titleElement) {
const randomSearchUrl = `${LRR_SERVER_URL}/api/search/random?filter=${encodeURIComponent(searchQuery)}`;
const headers = {};
if (LRR_API_KEY) {
headers['Authorization'] = `Bearer ${LRR_API_KEY}`;
}
try {
const response = await makeRequest({
method: 'GET',
url: randomSearchUrl,
headers: headers
});
try {
const randomResult = JSON.parse(response.responseText);
if (randomResult && randomResult.data && randomResult.data.length > 0) {
console.log(`[LRR Checker] Found via alternative search: ${searchQuery}`);
let altMarkerSpan = document.createElement('span');
altMarkerSpan.classList.add('lrr-marker-span');
altMarkerSpan.textContent = '(LRR!)';
altMarkerSpan.classList.add('lrr-marker-file');
titleElement.prepend(altMarkerSpan);
return { success: true, searchQuery };
} else {
console.log(`[LRR Checker] Not found via alternative search: ${searchQuery}`);
return { success: false, searchQuery };
}
} catch (e) {
console.error(`[LRR Checker] Error parsing JSON for alternative search:`, e, response.responseText);
return { success: false, searchQuery, error: e };
}
} catch (error) {
console.error(`[LRR Checker] Network error during alternative search:`, error);
return { success: false, searchQuery, error };
}
}
function handleResponse(result, titleElement, galleryUrl) {
let markerSpan = document.createElement('span');
markerSpan.classList.add('lrr-marker-span');
if (result.success === 1) {
console.log(`[LRR Checker] Found: ${galleryUrl} (ID: ${result.data.id})`);
markerSpan.textContent = '(LRR ✔)';
markerSpan.classList.add('lrr-marker-downloaded');
titleElement.prepend(markerSpan);
} else {
console.log(`[LRR Checker] Not found or error: ${galleryUrl} - ${result.error}`);
const fullTitle = titleElement.textContent.trim();
const authorRegex = /\[((?!汉化|漢化|DL版|中国翻訳)[^\]]+)\]/;
const authorMatch = fullTitle.match(authorRegex);
const author = authorMatch ? authorMatch[1] : null;
if (!author) {
console.log(`[LRR Checker] Skipping due to missing ${fullTitle}`);
return;
}
const titleRegex = /\]([^\[\]\(\)]+)/;
const titleMatch = fullTitle.match(titleRegex);
const title = titleMatch ? titleMatch[1] : null;
if (author === title || title === null) {
console.log(`[LRR Checker] Skipping due to missing ${fullTitle}`);
return;
}
const searchQuery = `${author},${title}`;
console.log(`[LRR Checker] Trying alternative search with: ${searchQuery}`);
// 执行备用搜索
performAlternativeSearch(searchQuery, titleElement);
}
}
})();