// ==UserScript==
// @name Mr. Banana Helper
// @namespace https://github.com/cailurus/MrBanana
// @version 0.2.0
// @description 在 JavDB 和 Jable 页面添加快捷按钮,一键发送到 Mr. Banana 服务
// @author xxm
// @match https://javdb.com/*
// @match https://*.javdb.com/*
// @match https://jable.tv/videos/*
// @match https://*.jable.tv/videos/*
// @icon https://raw.githubusercontent.com/cailurus/MrBanana/main/web/public/favicon.svg
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @connect *
// @license MIT
// @downloadURL https://raw.githubusercontent.com/cailurus/MrBanana/main/userscripts/mrbanana-helper.user.js
// @updateURL https://raw.githubusercontent.com/cailurus/MrBanana/main/userscripts/mrbanana-helper.user.js
// ==/UserScript==
(function () {
'use strict';
console.log('[MrBanana] 脚本已加载,当前页面:', window.location.href);
// =========================================================================
// 配置管理
// =========================================================================
const DEFAULT_SERVER = 'http://192.168.1.100:8000';
function getServerUrl() {
return GM_getValue('mrbanana_server', DEFAULT_SERVER);
}
function setServerUrl(url) {
GM_setValue('mrbanana_server', url);
}
function getConnectionStatus() {
return GM_getValue('mrbanana_status', null);
}
function setConnectionStatus(status) {
GM_setValue('mrbanana_status', status);
}
// =========================================================================
// 样式
// =========================================================================
GM_addStyle(`
/* 按钮样式 */
.mrbanana-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.mrbanana-btn-subscribe {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
}
.mrbanana-btn-subscribe:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
}
.mrbanana-btn-download {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
}
.mrbanana-btn-download:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.mrbanana-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.mrbanana-btn-success {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%) !important;
}
.mrbanana-btn-error {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
}
/* Toast 通知 */
.mrbanana-toast {
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
z-index: 999999;
animation: mrbanana-slide-in 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.mrbanana-toast-success { background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); }
.mrbanana-toast-error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.mrbanana-toast-info { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
@keyframes mrbanana-slide-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* 设置面板 */
.mrbanana-settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 999998;
display: flex;
align-items: center;
justify-content: center;
animation: mrbanana-fade-in 0.2s ease;
}
@keyframes mrbanana-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.mrbanana-settings-panel {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: 420px;
max-width: 90vw;
overflow: hidden;
animation: mrbanana-zoom-in 0.3s ease;
}
@keyframes mrbanana-zoom-in {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.mrbanana-settings-header {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
color: white;
padding: 20px 24px;
display: flex;
align-items: center;
gap: 12px;
}
.mrbanana-settings-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.mrbanana-settings-header .logo {
font-size: 28px;
}
.mrbanana-settings-close {
margin-left: auto;
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.mrbanana-settings-close:hover {
background: rgba(255,255,255,0.3);
}
.mrbanana-settings-body {
padding: 24px;
}
.mrbanana-form-group {
margin-bottom: 20px;
}
.mrbanana-form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
.mrbanana-form-hint {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.mrbanana-form-input {
width: 100%;
padding: 10px 14px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.mrbanana-form-input:focus {
outline: none;
border-color: #f59e0b;
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
}
.mrbanana-status-card {
background: #f9fafb;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
.mrbanana-status-row {
display: flex;
align-items: center;
gap: 10px;
}
.mrbanana-status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.mrbanana-status-indicator.online {
background: #22c55e;
box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);
}
.mrbanana-status-indicator.offline {
background: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
}
.mrbanana-status-indicator.checking {
background: #f59e0b;
animation: mrbanana-pulse 1s infinite;
}
@keyframes mrbanana-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.mrbanana-status-text {
font-size: 14px;
color: #374151;
}
.mrbanana-status-version {
margin-left: auto;
font-size: 12px;
color: #6b7280;
background: #e5e7eb;
padding: 2px 8px;
border-radius: 4px;
}
.mrbanana-btn-row {
display: flex;
gap: 12px;
}
.mrbanana-btn-primary {
flex: 1;
padding: 12px 20px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.mrbanana-btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
}
.mrbanana-btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.mrbanana-btn-secondary {
flex: 1;
padding: 12px 20px;
background: #f3f4f6;
color: #374151;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.mrbanana-btn-secondary:hover {
background: #e5e7eb;
}
`);
// =========================================================================
// 工具函数
// =========================================================================
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `mrbanana-toast mrbanana-toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function createButton(text, className, onClick) {
const btn = document.createElement('button');
btn.className = `mrbanana-btn ${className}`;
btn.innerHTML = `🍌 ${text}`;
btn.onclick = onClick;
return btn;
}
// =========================================================================
// 设置面板
// =========================================================================
function showSettingsPanel() {
// 移除已存在的面板
const existing = document.querySelector('.mrbanana-settings-overlay');
if (existing) existing.remove();
const currentUrl = getServerUrl();
const lastStatus = getConnectionStatus();
const overlay = document.createElement('div');
overlay.className = 'mrbanana-settings-overlay';
overlay.innerHTML = `
${lastStatus?.online ? '服务已连接' : '服务未连接'}
v${lastStatus?.version || ''}
`;
document.body.appendChild(overlay);
// 事件绑定
const closeBtn = overlay.querySelector('.mrbanana-settings-close');
const testBtn = overlay.querySelector('#mrbanana-test-btn');
const saveBtn = overlay.querySelector('#mrbanana-save-btn');
const input = overlay.querySelector('#mrbanana-server-input');
// 点击遮罩关闭
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.remove();
});
closeBtn.addEventListener('click', () => overlay.remove());
// 测试连接
testBtn.addEventListener('click', () => {
const url = input.value.trim().replace(/\/$/, '');
if (!url) {
showToast('请输入服务器地址', 'error');
return;
}
testConnection(url, overlay);
});
// 保存设置
saveBtn.addEventListener('click', () => {
const url = input.value.trim().replace(/\/$/, '');
if (!url) {
showToast('请输入服务器地址', 'error');
return;
}
setServerUrl(url);
showToast('设置已保存');
// 保存后自动测试
testConnection(url, overlay);
});
// ESC 关闭
const handleEsc = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('keydown', handleEsc);
}
function testConnection(url, overlay) {
const statusDot = overlay.querySelector('#mrbanana-status-dot');
const statusText = overlay.querySelector('#mrbanana-status-text');
const statusVersion = overlay.querySelector('#mrbanana-status-version');
const testBtn = overlay.querySelector('#mrbanana-test-btn');
// 设置检测中状态
statusDot.className = 'mrbanana-status-indicator checking';
statusText.textContent = '正在连接...';
statusVersion.style.display = 'none';
testBtn.disabled = true;
testBtn.textContent = '⏳ 检测中...';
GM_xmlhttpRequest({
method: 'GET',
url: `${url}/api/version`,
timeout: 10000,
onload: function (response) {
testBtn.disabled = false;
testBtn.textContent = '🔍 测试连接';
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
statusDot.className = 'mrbanana-status-indicator online';
statusText.textContent = '服务已连接';
statusVersion.textContent = `v${data.version || '?'}`;
statusVersion.style.display = '';
setConnectionStatus({ online: true, version: data.version });
showToast('连接成功!');
} catch (e) {
statusDot.className = 'mrbanana-status-indicator online';
statusText.textContent = '服务已连接(版本未知)';
setConnectionStatus({ online: true, version: null });
showToast('连接成功!');
}
} else {
statusDot.className = 'mrbanana-status-indicator offline';
statusText.textContent = `连接失败 (${response.status})`;
setConnectionStatus({ online: false });
showToast(`连接失败: HTTP ${response.status}`, 'error');
}
},
onerror: function (error) {
testBtn.disabled = false;
testBtn.textContent = '🔍 测试连接';
statusDot.className = 'mrbanana-status-indicator offline';
statusText.textContent = '无法连接到服务器';
setConnectionStatus({ online: false });
showToast('无法连接到服务器,请检查地址是否正确', 'error');
},
ontimeout: function () {
testBtn.disabled = false;
testBtn.textContent = '🔍 测试连接';
statusDot.className = 'mrbanana-status-indicator offline';
statusText.textContent = '连接超时';
setConnectionStatus({ online: false });
showToast('连接超时,请检查网络', 'error');
}
});
}
// 注册菜单命令
GM_registerMenuCommand('⚙️ Mr. Banana 设置', showSettingsPanel);
// =========================================================================
// JavDB 处理
// =========================================================================
function handleJavDB() {
console.log('[MrBanana] handleJavDB 开始执行');
// JavDB 新版页面结构: h2.title.is-4 > strong:first-child (第一个是番号)
const codeEl = document.querySelector('h2.title.is-4 > strong:first-child');
console.log('[MrBanana] 番号元素:', codeEl);
if (!codeEl) {
console.log('[MrBanana] 未找到番号元素,尝试备用选择器...');
// 尝试其他可能的选择器
const allH2 = document.querySelectorAll('h2');
console.log('[MrBanana] 页面上所有 h2:', allH2);
return;
}
const code = codeEl.textContent.trim();
console.log('[MrBanana] 番号:', code);
if (!code) return;
const titleSection = document.querySelector('h2.title.is-4');
console.log('[MrBanana] 标题区域:', titleSection);
if (!titleSection) return;
if (document.querySelector('.mrbanana-javdb-btn')) {
console.log('[MrBanana] 按钮已存在,跳过');
return;
}
console.log('[MrBanana] 准备添加按钮...');
const btn = createButton('订阅到 Mr. Banana', 'mrbanana-btn-subscribe mrbanana-javdb-btn', async () => {
const serverUrl = getServerUrl();
if (serverUrl === DEFAULT_SERVER) {
showToast('请先设置 Mr. Banana 服务器地址', 'info');
showSettingsPanel();
return;
}
btn.disabled = true;
btn.innerHTML = '🍌 订阅中...';
GM_xmlhttpRequest({
method: 'POST',
url: `${serverUrl}/api/subscription`,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ code: code }),
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
btn.innerHTML = '✓ 已订阅';
btn.classList.add('mrbanana-btn-success');
showToast(`已订阅 ${code}`);
} else {
let errorMsg = '订阅失败';
try {
const data = JSON.parse(response.responseText);
errorMsg = data.detail || errorMsg;
} catch (e) { }
btn.innerHTML = '✗ 失败';
btn.classList.add('mrbanana-btn-error');
showToast(errorMsg, 'error');
setTimeout(() => {
btn.innerHTML = '🍌 订阅到 Mr. Banana';
btn.classList.remove('mrbanana-btn-error');
btn.disabled = false;
}, 2000);
}
},
onerror: function () {
btn.innerHTML = '✗ 连接失败';
btn.classList.add('mrbanana-btn-error');
showToast('无法连接服务器', 'error');
setTimeout(() => {
btn.innerHTML = '🍌 订阅到 Mr. Banana';
btn.classList.remove('mrbanana-btn-error');
btn.disabled = false;
}, 2000);
}
});
});
btn.style.marginLeft = '12px';
titleSection.appendChild(btn);
}
// =========================================================================
// Jable 处理
// =========================================================================
function handleJable() {
console.log('[MrBanana] handleJable 开始执行');
const videoUrl = window.location.href;
if (!videoUrl.includes('/videos/')) {
console.log('[MrBanana] 不是视频页面,跳过');
return;
}
// 尝试多个可能的选择器
let titleSection = document.querySelector('.info-header h4, .video-info h4');
console.log('[MrBanana] 标题选择器1结果:', titleSection);
if (!titleSection) {
// 尝试更多选择器
titleSection = document.querySelector('h4.title, .detail-title h4, section.detail h4');
console.log('[MrBanana] 标题选择器2结果:', titleSection);
}
if (!titleSection) {
// 打印页面上所有 h4 元素帮助调试
const allH4 = document.querySelectorAll('h4');
console.log('[MrBanana] 页面上所有 h4:', allH4);
return;
}
if (document.querySelector('.mrbanana-jable-btn')) {
console.log('[MrBanana] 按钮已存在,跳过');
return;
}
console.log('[MrBanana] 准备添加 Jable 按钮...');
const btn = createButton('下载到 Mr. Banana', 'mrbanana-btn-download mrbanana-jable-btn', async () => {
const serverUrl = getServerUrl();
if (serverUrl === DEFAULT_SERVER) {
showToast('请先设置 Mr. Banana 服务器地址', 'info');
showSettingsPanel();
return;
}
btn.disabled = true;
btn.innerHTML = '🍌 添加中...';
// 先获取默认下载配置
GM_xmlhttpRequest({
method: 'GET',
url: `${serverUrl}/api/download/config`,
timeout: 10000,
onload: function (configResponse) {
let outputDir = '';
let scrapeAfter = false;
if (configResponse.status >= 200 && configResponse.status < 300) {
try {
const config = JSON.parse(configResponse.responseText);
outputDir = config.output_dir || '';
scrapeAfter = config.download_scrape_after_default || false;
console.log('[MrBanana] 获取到默认配置:', outputDir, scrapeAfter);
} catch (e) {
console.log('[MrBanana] 解析配置失败:', e);
}
}
if (!outputDir) {
btn.innerHTML = '✗ 未配置目录';
btn.classList.add('mrbanana-btn-error');
showToast('请先在 Mr. Banana 中设置默认下载目录', 'error');
setTimeout(() => {
btn.innerHTML = '🍌 下载到 Mr. Banana';
btn.classList.remove('mrbanana-btn-error');
btn.disabled = false;
}, 2000);
return;
}
// 发送下载请求
GM_xmlhttpRequest({
method: 'POST',
url: `${serverUrl}/api/download`,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
url: videoUrl,
output_dir: outputDir,
scrape_after_download: scrapeAfter
}),
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
btn.innerHTML = '✓ 已添加';
btn.classList.add('mrbanana-btn-success');
showToast('已添加到下载队列');
} else {
let errorMsg = '添加失败';
try {
const data = JSON.parse(response.responseText);
errorMsg = data.detail || errorMsg;
} catch (e) { }
btn.innerHTML = '✗ 失败';
btn.classList.add('mrbanana-btn-error');
showToast(errorMsg, 'error');
setTimeout(() => {
btn.innerHTML = '🍌 下载到 Mr. Banana';
btn.classList.remove('mrbanana-btn-error');
btn.disabled = false;
}, 2000);
}
},
onerror: function () {
btn.innerHTML = '✗ 连接失败';
btn.classList.add('mrbanana-btn-error');
showToast('无法连接服务器', 'error');
setTimeout(() => {
btn.innerHTML = '🍌 下载到 Mr. Banana';
btn.classList.remove('mrbanana-btn-error');
btn.disabled = false;
}, 2000);
}
});
},
onerror: function () {
btn.innerHTML = '✗ 连接失败';
btn.classList.add('mrbanana-btn-error');
showToast('无法连接服务器', 'error');
setTimeout(() => {
btn.innerHTML = '🍌 下载到 Mr. Banana';
btn.classList.remove('mrbanana-btn-error');
btn.disabled = false;
}, 2000);
}
});
});
btn.style.marginLeft = '12px';
btn.style.display = 'inline-flex';
titleSection.parentNode.insertBefore(btn, titleSection.nextSibling);
}
// =========================================================================
// 初始化
// =========================================================================
function init() {
const hostname = window.location.hostname;
if (hostname.includes('javdb')) {
setTimeout(handleJavDB, 1000);
const observer = new MutationObserver(() => {
setTimeout(handleJavDB, 500);
});
observer.observe(document.body, { childList: true, subtree: true });
} else if (hostname.includes('jable')) {
setTimeout(handleJable, 1000);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();