// ==UserScript==
// @name JavSS
// @namespace javss
// @version 1.3
// @description 为 javdb、javbus 添加视频缩略图,菜单栏可直接按番号查询
// @author anonymous
// @license MIT
// @match https://javdb.com/*
// @match https://javdb459.com/*
// @match https://www.javbus.com/*
// @match https://www.javsee.ink/*
// @icon http://javdb459.com/favicon.ico
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @connect *
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM.xmlHttpRequest
// ==/UserScript==
(function () {
'use strict';
const DEBUG_MODE = false;
const SITE_CONFIGS = {
'javdb.com': {
idSelector: 'div.video-detail > h2 > strong',
getId: (element) => element.textContent.trim().split(' ')[0],
insertLink: ['javbus', 'avwiki', 'nyaa'],
insertLinkSelector: 'div.panel-block.first-block',
insertSearchSelector: '#navbar-menu-hero > .navbar-start',
insertImageSelector: 'div.video-meta-panel',
preCheck: {
selector: 'section > div > div.movie-list',
action: (movieList) => modifyMovieList(movieList)
},
},
'www.javbus.com': {
idSelector: 'div.info > p:nth-child(1) > span:nth-child(2)',
getId: (element) => element.textContent,
insertLink: ['javdb', 'avwiki', 'nyaa'],
insertSearchSelector: '#navbar',
insertImageSelector: 'div.row.movie',
}
// 其他网站添加在这里
};
const DOMAIN_MAPPING = {
'javdb459.com': 'javdb.com',
'www.dmmbus.ink': 'www.javbus.com',
'www.seejav.ink': 'www.javbus.com',
'www.javsee.ink': 'www.javbus.com'
};
const SITE_STYLES = {
'javdb.com': `
.ssc {
text-align: center;
padding: 2rem 0 3rem;
border-top: 1px dashed #4a4a4a;
}
.ss {
width: 95%;
min-width: 9rem;
min-height: 1rem;
background-color: #4a4a4a;
}
#search-ss {
color: inherit;
font-size: inherit;
background: none;
border: none;
cursor: pointer;
}
`,
'www.javbus.com': `
.ss {
margin-top: 15px;
min-width: 9rem;
min-height: 1rem;
background-color: #4a4a4a;
}
#search-ss {
padding: 15px;
background: none;
border: none;
}
`
};
const SEARCH_ENGINE = {
'javdb': 'https://javdb459.com/search?q=',
'javbus': 'https://www.javbus.com/',
'nyaa': 'https://sukebei.nyaa.si/?q=',
'avwiki': 'https://av-wiki.net/?s='
};
//- 缩略图源配置
const IMAGE_SOURCES = [
{
name: 'javbee',
url: 'https://javbee.vip/search?keyword=',
stages: [
{
selector: 'div.images-description > ul > li:last-child > a',
getContent: (element) => element.innerHTML.trim()
},
{
selector: 'div.fileviewer-file > img',
getContent: (element) => element.src,
},
],
},
{
name: 'javstore new',
url: 'https://javstore.net/search/',
buildUrl: (id) => `https://javstore.net/search/${id}.html`,
stages: [
{
selector: '#content_news > ul > li:last-child > a',
getContent: (element) => element.href,
},
{
selector: 'div.news > a',
getContent: (element) => element.href,
},
],
},
{
name: '3xplanet',
url: 'https://3xplanet.net/',
stages: [
{
selector: 'div.td-post-content > div > p:nth-child(2) > a',
isRelative: true,
getContent: (element) => element.getAttribute('href'),
},
{
selector: '#show_image',
getContent: (element) => element.src,
},
],
},
// {
// name: 'javstore old',
// url: 'https://javstore.net/search/',
// buildUrl: (id) => `https://javstore.net/search/${id}.html`,
// stages: [
// {
// selector: '#content_news > ul > li:last-child > a',
// getContent: (element) => element.href,
// },
// {
// selector: 'div.news > a',
// getContent: (element) => element.href,
// },
// {
// isRedirect: true,
// selector: '#embed-code-2',
// getContent: (element) => element.getAttribute('value'),
// }
// ],
// },
];
const Swal = window.Swal;
if (!Swal) console.error("SweetAlert 未加载");
//==========================================================================
const createLink = (title, href) => {
const link = document.createElement('a');
// link.target = '_blank';
link.href = href;
link.title = title;
link.textContent = ' 🔗';
link.style.color = 'inherit';
return link;
};
const addLink = (target, engines, id) => {
engines.forEach(engine => {
const a = createLink(engine, SEARCH_ENGINE[engine] + id);
target.append(a);
});
};
const processItem = (item) => {
const id = item.querySelector('div.video-title > strong').textContent;
const tagsContainer = item.querySelector('div.tags');
const newTag = document.createElement('span');
newTag.classList.add('tag', 'is-info');
newTag.textContent = '缩略图';
newTag.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const { imgSrc, refUrl } = await getImage(id);
if (imgSrc) {
showImagePopup(id, imgSrc, refUrl);
}
});
tagsContainer.append(newTag);
};
const modifyMovieList = (movieList) => {
const container = document.querySelector('section > div');
if (container) { container.style.maxWidth = '100%'; }
const items = Array.from(movieList.querySelectorAll('.item'));
if (items.length === 0) return;
let currentIndex = 0;
const itemsPerRow = 4; // 每行4个元素
const scrollToCurrent = () => {
if (items[currentIndex]) {
items[currentIndex].scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
};
const nextPage = () => {
const nextPageLink = document.querySelector('a.pagination-next');
nextPageLink?.click();
};
const prevPage = () => {
const prevPageLink = document.querySelector('a.pagination-previous');
prevPageLink?.click();
};
document.addEventListener('auxclick', (e) => {
switch (e.button) {
case 4: // 鼠标侧键前进
nextPage();
e.preventDefault();
break;
}
});
document.addEventListener('keydown', function (e) {
switch (e.key) {
case 'ArrowRight':
nextPage();
break;
case 'ArrowLeft':
prevPage();
break;
case 'ArrowDown':
currentIndex = Math.min(currentIndex + itemsPerRow, items.length - 1);
scrollToCurrent();
e.preventDefault();
break;
case 'ArrowUp':
currentIndex = Math.max(currentIndex - itemsPerRow, 0);
scrollToCurrent();
e.preventDefault();
break;
case 'Home':
currentIndex = 0;
break;
case 'End':
currentIndex = items.length - 1;
break;
}
});
items.forEach(processItem);
};
const handleResponseError = (statusCode, url) => {
const errorMessages = {
400: '错误请求 - 客户端发送了无效的请求(可能是参数错误)',
401: '未授权 - 需要身份验证',
403: '禁止访问 - 服务器拒绝请求',
404: '未找到 - 请求的资源不存在',
500: '服务器内部错误 - 服务器端出现问题',
502: '网关错误 - 上游服务器无效响应',
503: '服务不可用 - 服务器暂时过载或维护',
504: '网关超时 - 上游服务器未及时响应'
};
const errorType = statusCode >= 500
? '服务器端错误'
: statusCode >= 400
? '客户端错误'
: '未知错误';
const message = errorMessages[statusCode] || errorType;
console.warn(`请求失败: ${url} (状态码 ${statusCode}) - ${message}`);
};
const resolveUrl = (currentUrl, pathname) => {
try {
return pathname.startsWith('http')
? pathname
: new URL(pathname, new URL(currentUrl).origin).href;
} catch (error) {
console.error(`URL resolution failed for currentUrl: ${currentUrl}, path: ${pathname}`);
return null;
}
};
const getImage = async (id, fromCache = true) => {
if (!id || typeof id !== 'string') {
throw new Error('番号无效');
}
// 默认启用尝试从缓存获取地址
if (fromCache && !DEBUG_MODE) {
const cachedResult = JSON.parse(localStorage.getItem('GM_' + id));
if (cachedResult) {
console.log('从缓存获取到缩略图地址:', cachedResult.imgSrc, '@', cachedResult.refUrl);
return cachedResult;
}
}
/* eslint-disable no-await-in-loop */
for (const source of IMAGE_SOURCES) {
const startTime = performance.now();
const initialUrl = source.buildUrl?.(id) ?? `${source.url}${id}`;
console.group('Source', source.name);
try {
let currentUrl = initialUrl;
let currentContent = null;
for (const [index, stage] of source.stages.entries()) {
console.group('Stage', index + 1);
console.log('请求地址:', currentUrl);
try {
const response = await GM.xmlHttpRequest({
method: 'GET',
url: currentUrl,
timeout: 5000,
...(stage.isRedirect ? { redirect: 'follow' } : {})
});
if (stage.isRedirect && response.finalUrl !== currentUrl) {
console.log(`↳ 重定向到: ${response.finalUrl}`);
}
if (response.status !== 200) {
handleResponseError(response.status, currentUrl);
break;
}
const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
const element = doc.querySelector(stage.selector);
if (!element) {
console.warn(`未在地址找到元素: ${currentUrl} : ${stage.selector}`);
break;
}
currentContent = stage.getContent(element);
// 如果是最后阶段则直接返回
if (index === source.stages.length - 1) {
console.groupEnd(); // 结束 stage 组
console.groupEnd(); // 结束 source 组
if (DEBUG_MODE) {
console.log(`${source.name} 获取到地址: ${currentContent} @ ${currentUrl}`);
} else {
console.log('获取到缩略图地址:', currentContent, '@', currentUrl);
const result = {
imgSrc: currentContent,
refUrl: currentUrl
};
localStorage.setItem('GM_' + id, JSON.stringify(result));
return result;
}
}
// 处理下一阶段请求地址 URL
currentUrl = stage.isRelative
? resolveUrl(currentUrl, currentContent)
: currentContent;
if (stage.isRedirect) {
currentUrl = response.finalUrl;
}
if (!currentUrl) break;
} catch (error) {
console.error('Stage error:', error);
throw error;
} finally {
console.groupEnd(); // 确保每个 stage 组都会关闭
}
}
} catch (error) {
console.error('Source processing failed:', error);
} finally {
const duration = performance.now(); - startTime;
if (DEBUG_MODE) {
console.log(`[耗时统计] ${source.name}: ${duration.toFixed(2)}ms`);
}
console.groupEnd(); // 确保每个 source 组都会关闭
}
}
/* eslint-enable no-await-in-loop */
throw new Error('所有图源尝试获取失败');
};
const addImage = async (id, selector) => {
console.group(id);
try {
const { imgSrc, refUrl } = await getImage(id);
if (!imgSrc) return;
const container = document.createElement('div');
container.classList.add('ssc');
const a = document.createElement('a');
a.href = refUrl;
a.title = '点击跳转到来源';
const img = document.createElement('img');
img.classList.add('ss');
img.src = imgSrc;
img.alt = '点击跳转到来源';
const target = document.querySelector(selector);
target.append(container);
container.append(a);
a.append(img);
} catch (error) {
console.error('添加缩略图失败:', error.message);
return null;
} finally {
console.groupEnd();
}
};
const showImagePopup = (id, imgSrc, refUrl) => {
Swal.fire({
theme: 'auto',
width: '1224px',
draggable: true,
animation: false,
showCancelButton: true,
cancelButtonText: '关闭',
imageUrl: imgSrc,
imageAlt: '点击跳转到来源',
imageWidth: '100%',
confirmButtonText: '跳转到来源',
preConfirm: () => {
window.open(refUrl, '_blank');
return false;
},
showDenyButton: true,
denyButtonText: '清除缓存地址',
preDeny: async () => {
localStorage.removeItem('GM_' + id);
await Swal.fire({
timer: 1500,
theme: 'auto',
icon: 'success',
title: '操作完成',
text: '缩略图缓存地址已清除',
showConfirmButton: false,
});
return true;
}
});
};
const showSearchDialog = async () => {
const { value: response } = await Swal.fire({
theme: 'auto',
title: '查找',
width: '20.5em',
input: 'text',
confirmButtonText: '查找',
inputPlaceholder: '输入番号',
showLoaderOnConfirm: true,
preConfirm: async (id) => {
try {
const response = await getImage(id);
response.id = id;
return (Swal.isVisible()) ? response : false;
} catch (error) {
Swal.showValidationMessage(`请求失败: ${error.message}`);
}
},
showDenyButton: true,
denyButtonColor: '#3085d6',
returnInputValueOnDeny: true,
denyButtonText: '强制刷新',
preDeny: async (id) => {
try {
const response = await getImage(id, false);
response.id = id;
return (Swal.isVisible()) ? response : false;
} catch (error) {
Swal.showValidationMessage(`请求失败: ${error.message}`);
}
},
showCancelButton: true,
cancelButtonText: '取消',
// allowOutsideClick: () => !Swal.isLoading()
});
if (response) {
showImagePopup(response.id, response.imgSrc, response.refUrl);
}
};
const init = async () => {
GM_registerMenuCommand('查找缩略图', showSearchDialog);
};
//==========================================================================
const main = () => {
init();
const hostname = window.location.hostname;
const host = DOMAIN_MAPPING?.[hostname] ?? hostname;
const site = SITE_CONFIGS[host];
if (!site) {
console.log('需添加网站配置到 SITE_CONFIGS');
return;
}
GM_addStyle(SITE_STYLES[host]);
if (site.insertSearchSelector) {
const target = document.querySelector(site.insertSearchSelector);
if (target) {
const button = document.createElement("button");
button.textContent = "查找缩略图";
button.id = 'search-ss';
button.addEventListener("click", showSearchDialog);
target.append(button);
}
}
if (site.preCheck) {
const target = document.querySelector(site.preCheck.selector);
if (target) {
site.preCheck.action(target);
}
}
const idElement = document.querySelector(site.idSelector);
if (!idElement) return;
const id = site.getId(idElement);
if (!id) return;
addLink(site.insertLinkSelector ? document.querySelector(site.insertLinkSelector) : idElement, site.insertLink, id);
addImage(id, site.insertImageSelector);
};
main();
})();