Greasy Fork is available in English.
影巢显示emby已入库以及未入库 支持主页,详情页,用户页面,合集页面,添加一键转存 115 按钮转存到 115 网盘,ui 可选文件夹 id 支持日志显示, 支持并适配移动端与pc端
// ==UserScript==
// @name 影巢 Emby&115 转存助手
// @version 1.6.3
// @description 影巢显示emby已入库以及未入库 支持主页,详情页,用户页面,合集页面,添加一键转存 115 按钮转存到 115 网盘,ui 可选文件夹 id 支持日志显示, 支持并适配移动端与pc端
// @author 楠
// @match *://hdhive.com/*
// @match *://www.hdhive.com/*
// @match *://115.com/s/*
// @match *://115cdn.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant window.close
// @grant window.opener
// @license MIT
// @icon https://hdhive.com/apple-touch-icon.png
// @namespace http://greasyfork.icu/users/1514724
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
targetDomain: 'hdhive.com',
autoCloseDelay: 1500,
maxWaitTime: 15000,
api115: 'https://115cdn.com/webapi/share/receive',
snap115: 'https://115cdn.com/webapi/share/snap',
symediaApiPath: '/api/v1/plugin/cloud_helper/add_share_urls_115'
};
let EMBY_CONFIG = {
HOST: GM_getValue("embyHost", ""),
API_KEY: GM_getValue("embyApiKey", "")
};
const state = {
processingItems: new Set(),
processedItems: new Set(),
embyCache: new Map(),
transferButtonsInitialized: false,
currentFilter: 'all'
};
const BUTTON_STYLES = {
posterBtn: {
size: '25px',
position: { top: '10px', right: '10px' },
has: {
bg: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
icon: '✓',
border: '2px solid rgba(255,255,255,0.8)'
},
notHas: {
bg: 'linear-gradient(135deg, #F44336 0%, #C62828 100%)',
icon: '✗',
border: '2px solid rgba(255,255,255,0.8)'
},
hoverEffect: 'scale(1.1)'
},
nameBtn: {
padding: '3px 10px',
marginTop: '5px',
fontSize: '11px',
has: {
bg: 'rgba(76, 175, 80, 0.15)',
text: '已入库',
textColor: '#4CAF50',
border: '1px solid rgba(76, 175, 80, 0.3)'
},
notHas: {
bg: 'rgba(244, 67, 54, 0.15)',
text: '未入库',
textColor: '#F44336',
border: '1px solid rgba(244, 67, 54, 0.3)'
},
hoverEffect: 'translateY(-1px)'
},
detailBtn: {
posterBtn: {
size: '30px',
position: { top: '15px', right: '15px' },
has: {
bg: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
icon: '✓',
border: '2px solid rgba(255,255,255,0.9)'
},
notHas: {
bg: 'linear-gradient(135deg, #F44336 0%, #C62828 100%)',
icon: '✗',
border: '2px solid rgba(255,255,255,0.9)'
},
hoverEffect: 'scale(1.15)'
},
titleBtn: {
padding: '5px 15px',
marginLeft: '10px',
fontSize: '12px',
has: {
bg: 'rgba(76, 175, 80, 0.15)',
text: '已入库',
textColor: '#4CAF50',
border: '1px solid rgba(76, 175, 80, 0.4)'
},
notHas: {
bg: 'rgba(244, 67, 54, 0.15)',
text: '未入库',
textColor: '#F44336',
border: '1px solid rgba(244, 67, 54, 0.4)'
},
hoverEffect: 'translateY(-1px)'
}
},
searchYearBtn: {
padding: '3px 10px',
marginLeft: '10px',
fontSize: '11px',
has: {
bg: 'rgba(76, 175, 80, 0.15)',
text: '已入库',
textColor: '#4CAF50',
border: '1px solid rgba(76, 175, 80, 0.3)'
},
notHas: {
bg: 'rgba(244, 67, 54, 0.15)',
text: '未入库',
textColor: '#F44336',
border: '1px solid rgba(244, 67, 54, 0.3)'
},
hoverEffect: 'translateY(-1px)'
},
userPageBtn: {
padding: '3px 10px',
marginLeft: '8px',
fontSize: '11px',
has: {
bg: 'rgba(76, 175, 80, 0.35)',
text: '已入库',
textColor: '#4CAF50',
border: '1px solid rgba(76, 175, 80, 0.4)'
},
notHas: {
bg: 'rgba(244, 67, 54, 0.35)',
text: '未入库',
textColor: '#F44336',
border: '1px solid rgba(244, 67, 54, 0.4)'
},
hoverEffect: 'translateY(-1px)'
},
collectionBtn: {
padding: '3px 10px',
marginLeft: '10px',
fontSize: '11px',
has: {
bg: 'rgba(76, 175, 80, 0.15)',
text: '已入库',
textColor: '#4CAF50',
border: '1px solid rgba(76, 175, 80, 0.3)'
},
notHas: {
bg: 'rgba(244, 67, 54, 0.15)',
text: '未入库',
textColor: '#F44336',
border: '1px solid rgba(244, 67, 54, 0.3)'
},
hoverEffect: 'translateY(-1px)'
},
settingBtn: {
padding: '6px 16px',
marginRight: '10px',
fontSize: '12px',
has: {
bg: 'rgba(100, 181, 246, 0.35)',
text: '设置',
textColor: '#64B5F6',
border: '1px solid rgba(100, 181, 246, 0.4)'
},
hoverEffect: 'translateY(-1px)',
iconSize: '16px'
},
transferBtn: {
padding: '3px 10px',
marginLeft: '4px',
fontSize: '11px',
bg: 'rgba(227, 242, 253, 1)',
textColor: '#0d47a1',
border: '1px solid rgba(13, 71, 161, 0.3)',
hoverEffect: 'translateY(-1px)',
iconSize: '16px'
}
};
const Utils = {
normalizeText: (t = '') => String(t).replace(/\s+/g, '').trim().toLowerCase(),
isSafari: (() => {
try {
const ua = navigator.userAgent;
return /Safari/.test(ua) && !/Chrome|Chromium|Edg|OPR|Android/.test(ua);
} catch (e) {
return false;
}
})(),
isDetailPage: () => {
const path = window.location.pathname;
return /^\/(movie|tv)\/[\w-]+/.test(path);
},
isUserPage: () => window.location.pathname.startsWith('/user/'),
isCollectionPage: () => window.location.pathname.startsWith('/collection/'),
isResourcePage: () => /\/resource\/(115\/)?[\w-]+/.test(location.pathname),
isFinal115Page: () => location.href.includes('115cdn.com') || location.href.includes('115.com/s/'),
isParentPage: () => {
const href = location.href;
const isDetailPage =
(href.includes('/movie/') && href.split('/movie/').length > 1) ||
(href.includes('/tv/') && href.split('/tv/').length > 1);
return href.includes(CONFIG.targetDomain) && isDetailPage && !href.includes('/resource/');
},
isHDHiveSite: () => {
return location.hostname.includes('hdhive.com') &&
!Utils.isResourcePage() &&
!Utils.isFinal115Page();
},
verifyAndFormatUrl: (rawUrl) => {
try {
if (!rawUrl) return { success: false, msg: "链接为空" };
const urlObj = new URL(rawUrl);
if (!urlObj.hostname.includes('115')) return { success: false, msg: "非115域名" };
const pickcode = urlObj.pathname.split('/').pop();
if (!pickcode) return { success: false, msg: "无法提取Pickcode" };
const search = urlObj.search;
if (!search || !search.includes('=')) return { success: false, msg: "链接未包含密码(Key)" };
const lastEqualIndex = rawUrl.lastIndexOf('=');
let potentialPass = rawUrl.substring(lastEqualIndex + 1);
if (potentialPass.length >= 4) {
var password = potentialPass.substring(0, 4);
} else {
return { success: false, msg: "密码长度不足" };
}
return { success: true, url: `https://115.com/s/${pickcode}?password=${password}`, msg: "格式化成功" };
} catch (e) { return { success: false, msg: `解析异常: ${e.message}` }; }
},
parseShareLink: (shareLink) => {
const shareCodeMatch = shareLink.match(/\/s\/([^?]+)/);
const passwordMatch = shareLink.match(/password=(\w{4})/);
if (!shareCodeMatch || !passwordMatch) return { success: false };
return {
success: true,
shareCode: shareCodeMatch[1],
receiveCode: passwordMatch[1]
};
},
humanReadable: (size) => {
if (size < 1024) return `${size}B`;
else if (size < 1024**2) return `${(size/1024).toFixed(2)}KB`;
else if (size < 1024**3) return `${(size/1024/1024).toFixed(2)}MB`;
else if (size < 1024**4) return `${(size/1024/1024/1024).toFixed(2)}GB`;
else if (size < 1024**5) return `${(size/1024/1024/1024/1024).toFixed(2)}TB`;
else return `${(size/1024/1024/1024/1024/1024).toFixed(2)}PB`;
},
normalizeUrl: (url) => {
if (!url) return '';
return url.replace(/\/+$/, '');
},
getDiskType: (el) => {
const html = el.innerHTML;
if (html.includes('M34.694') || html.includes('115网盘') || html.includes('一生相伴')) return '115';
if (html.includes('M4.240')) return '123';
if (html.includes('M833.399') || html.includes('天翼')) return 'tianyi';
if (html.includes('M513.465')) return 'baidu';
if (html.includes('M466.483') || html.includes('夸克')) return 'quark';
if (html.includes('M841.984') || el.innerText.includes('阿里')) return 'ali';
return 'other';
}
};
const Logger = {
stats: { free: 0, paid: 0, unlocked: 0 },
processedLinks: new Set(),
currentTaskId: null,
logPanel: null,
logContent: null,
init: () => {
Logger.createLogPanel();
},
createLogPanel: () => {
const logPanel = document.createElement('div');
logPanel.id = 'hdhive-log-panel';
Object.assign(logPanel.style, {
position: 'fixed',
bottom: '85px',
right: '20px',
width: '380px',
height: '380px',
backgroundColor: 'white',
boxShadow: '0 4px 15px rgba(0,0,0,0.2)',
borderRadius: '8px',
zIndex: 99999,
display: 'none',
flexDirection: 'column',
fontFamily: 'sans-serif',
border: '1px solid #ddd',
fontSize: '12px'
});
logPanel.innerHTML = `
<div style="padding:10px;border-bottom:1px solid #eee;background:#f1f3f5;
border-radius:8px 8px 0 0;font-weight:bold;display:flex;justify-content:space-between;">
<span>🤖 115转存助手日志</span>
<span style="cursor:pointer;user-select:none;" onclick="document.getElementById('hdhive-log-panel').style.display='none';">关闭</span>
</div>
<div id="log-stats" style="padding:8px;border-bottom:1px solid #eee;display:flex;gap:10px;background:#fff;flex-wrap:wrap;">
<span id="stat-free" style="color:#2e7d32;display:none;">免费: 0</span>
<span id="stat-paid" style="color:#d32f2f;display:none;">付费: 0</span>
<span id="stat-unlocked" style="color:#1976d2;display:none;">已解锁: 0</span>
</div>
<div id="log-content" style="flex:1;overflow-y:auto;padding:10px;line-height:1.6;background:#fafafa;"></div>
`;
document.body.appendChild(logPanel);
Logger.logPanel = logPanel;
Logger.logContent = document.getElementById('log-content');
},
showLogPanel: () => {
if (Logger.logPanel) {
Logger.logPanel.style.display = 'flex';
}
},
hideLogPanel: () => {
if (Logger.logPanel) {
Logger.logPanel.style.display = 'none';
}
},
toggleLogPanel: () => {
if (Logger.logPanel.style.display === 'flex') {
Logger.hideLogPanel();
} else {
Logger.showLogPanel();
}
},
startNewTask: (resourceUrl) => {
const taskId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
Logger.currentTaskId = taskId;
return taskId;
},
endCurrentTask: (status = 'completed') => {
if (Logger.currentTaskId) {
Logger.currentTaskId = null;
}
},
addLog: (msg, type = 'info') => {
if (!Logger.logContent) return;
const entry = document.createElement('div');
entry.style.borderBottom = '1px dashed #eee';
entry.style.padding = '2px 0';
const time = new Date().toLocaleTimeString();
let color = ({error:'#d32f2f',success:'#2e7d32',process:'#0288d1'})[type]||'#333';
entry.innerHTML = `<span style="color:#999">[${time}]</span> <span style="color:${color}">${msg}</span>`;
Logger.logContent.appendChild(entry);
Logger.logContent.scrollTop = Logger.logContent.scrollHeight;
},
updateStats: (type) => {
const statElement = document.getElementById(`stat-${type}`);
if (Logger.stats[type]!==undefined && statElement) {
Logger.stats[type]++;
statElement.textContent = `${type==='free'?'免费':(type==='paid'?'付费':'已解锁')}: ${Logger.stats[type]}`;
statElement.style.display='inline';
}
},
isLinkProcessed: (link) => {
return Logger.processedLinks.has(link);
},
markLinkAsProcessed: (link) => {
Logger.processedLinks.add(link);
}
};
const Transfer115 = {
transfer: async (shareLink, cookie, targetCid) => {
const parseResult = Utils.parseShareLink(shareLink);
if (!parseResult.success) {
return {
success: false,
message: '无法解析分享链接或密码',
file_size: ''
};
}
const { shareCode, receiveCode } = parseResult;
return new Promise((resolve) => {
const postData = new URLSearchParams({
share_code: shareCode,
receive_code: receiveCode,
cid: targetCid,
is_check: 0
});
GM_xmlhttpRequest({
method: "POST",
url: CONFIG.api115,
headers: {
"Cookie": cookie,
"Content-Type": "application/x-www-form-urlencoded"
},
data: postData.toString(),
onload: (response) => {
try {
const respJson = JSON.parse(response.responseText);
if (respJson.state === true) {
Transfer115.getFileSize(shareCode, receiveCode, cookie).then(fileSize => {
resolve({
success: true,
message: `✅ 115转存成功[${fileSize}]`,
file_size: fileSize
});
}).catch(err => {
resolve({
success: true,
message: '✅ 115转存成功[大小未知]',
file_size: ''
});
});
} else if (respJson.errno === 4100024) {
resolve({
success: false,
message: '⚠️ 转存失败:你已经转存过该文件',
file_size: ''
});
} else if (respJson.errno === 4100008) {
resolve({
success: false,
message: '❌ 转存失败:分享链接密码错误',
file_size: ''
});
} else if (respJson.errno === 4100010) {
resolve({
success: false,
message: '❌ 转存失败:分享已取消',
file_size: ''
});
} else if (respJson.errno === 4100018) {
resolve({
success: false,
message: '❌ 转存失败:链接已过期',
file_size: ''
});
} else {
resolve({
success: false,
message: `❌ 转存失败: ${respJson.error || '未知错误'}`,
file_size: ''
});
}
} catch (e) {
resolve({
success: false,
message: `❌ 转存异常: ${e.message}`,
file_size: ''
});
}
},
onerror: (error) => {
resolve({
success: false,
message: '❌ 转存接口调用失败',
file_size: ''
});
}
});
});
},
getFileSize: async (shareCode, receiveCode, cookie) => {
return new Promise((resolve, reject) => {
const snapParams = {
"_v": 2,
"share_code": shareCode,
"receive_code": receiveCode,
"offset": 0,
"limit": 20,
"cid": ""
};
const queryString = new URLSearchParams(snapParams).toString();
GM_xmlhttpRequest({
method: "GET",
url: `${CONFIG.snap115}?${queryString}`,
headers: {
"Cookie": cookie,
"Referer": "https://115.com/",
"Origin": "https://115.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
},
onload: (response) => {
try {
const fileInfoJson = JSON.parse(response.responseText);
if (fileInfoJson.state && fileInfoJson.data && fileInfoJson.data.list && fileInfoJson.data.list[0]) {
const fileSize = fileInfoJson.data.list[0].s || 0;
resolve(Utils.humanReadable(fileSize));
} else {
if (fileInfoJson.data && fileInfoJson.data[0]) {
const fileSize = fileInfoJson.data[0].s || fileInfoJson.data[0].size || 0;
resolve(Utils.humanReadable(fileSize));
} else {
reject(new Error('无法获取文件大小'));
}
}
} catch (e) {
reject(e);
}
},
onerror: (error) => {
reject(error);
}
});
});
},
transferBySymedia: async (shareLink, symediaUrl, symediaToken, targetCid) => {
if (!symediaUrl || !symediaToken) {
return {
success: false,
message: 'Symedia配置不完整'
};
}
const normalizedUrl = Utils.normalizeUrl(symediaUrl);
const apiUrl = `${normalizedUrl}${CONFIG.symediaApiPath}?token=${symediaToken}`;
return new Promise((resolve) => {
const postData = JSON.stringify({
urls: [shareLink],
parent_id: targetCid ? String(targetCid) : '0'
});
GM_xmlhttpRequest({
method: "POST",
url: apiUrl,
headers: {
"Content-Type": "application/json"
},
data: postData,
onload: (response) => {
try {
const respJson = JSON.parse(response.responseText);
if (response.status === 200 && respJson.success === true) {
if (respJson.message && respJson.message.includes('转存失败')) {
resolve({
success: false,
message: `❌ Symedia转存失败: ${respJson.message}`
});
} else {
resolve({
success: true,
message: `✅ Symedia转存: ${respJson.message}`
});
}
} else {
resolve({
success: false,
message: `❌ Symedia转存失败: ${respJson.message || '未知错误'}`
});
}
} catch (e) {
resolve({
success: false,
message: `❌ Symedia转存异常: ${e.message}`
});
}
},
onerror: (error) => {
resolve({
success: false,
message: '❌ Symedia接口调用失败'
});
}
});
});
}
};
const FilterManager = {
injectBar: () => {
if (!Utils.isUserPage() || document.getElementById('hdhive-filter-bar')) return;
const anyResource = document.querySelector('a[href*="/resource/"]');
const grid = anyResource?.closest('.MuiGrid-container');
if (grid && grid.parentElement) {
const bar = document.createElement('div'); bar.id = 'hdhive-filter-bar';
bar.style.cssText = 'display:inline-flex; align-items:center; gap:6px; margin:0 0 20px 0; padding:10px; background:rgba(128,128,128,0.1); border-radius:12px; box-sizing:border-box; width:fit-content; flex-wrap:wrap; backdrop-filter:blur(4px);';
const opts = [{l:'全部', id:'all'}, {l:'115', id:'115'}, {l:'123', id:'123'}, {l:'天翼', id:'tianyi'}, {l:'百度', id:'baidu'}, {l:'夸克', id:'quark'}, {l:'阿里', id:'ali'}, {l:'其他', id:'other'}];
opts.forEach(opt => {
const btn = document.createElement('div'); btn.className = 'hd-filter-btn';
btn.innerHTML = `<span style="line-height:20px;">${opt.l}</span>`;
btn.style.cssText = `padding:6px 16px; border-radius:8px; font-size:13px; font-weight:bold; cursor:pointer; transition:0.2s; color:${state.currentFilter===opt.id?'#fff':'#555'}; background:${state.currentFilter===opt.id?'#1890ff':'transparent'};`;
btn.onclick = async () => {
state.currentFilter = opt.id;
document.querySelectorAll('.hd-filter-btn').forEach(b => { b.style.background='transparent'; b.style.color='#555'; });
btn.style.background='#1890ff'; btn.style.color='#fff';
FilterManager.apply();
state.processedItems.clear();
state.processingItems.clear();
document.querySelectorAll('.emby-user-page-btn, .emby-poster-btn, .one-click-transfer-btn').forEach(b => b.remove());
await processUserPageButtons();
};
btn.title = opt.l;
bar.appendChild(btn);
});
grid.parentElement.insertBefore(bar, grid);
FilterManager.apply();
}
},
apply: () => {
document.querySelectorAll('a[href*="/resource/"]').forEach(link => {
const card = link.closest('div[class*="Grid"] > div');
if(!card) return;
const type = Utils.getDiskType(card);
if (state.currentFilter === 'all' || state.currentFilter === type) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}
};
const EmbyHelper = {
checkEmbyResource: (name, year) => {
return new Promise((resolve) => {
const cacheKey = `${name}-${year}`;
if (state.embyCache.has(cacheKey)) {
resolve(state.embyCache.get(cacheKey));
return;
}
const searchUrl = `${EMBY_CONFIG.HOST}/emby/Items?api_key=${EMBY_CONFIG.API_KEY}&SearchTerm=${encodeURIComponent(name)}&IncludeItemTypes=Movie,Series&Recursive=true&Fields=ProductionYear,OriginalTitle&Limit=20`;
GM_xmlhttpRequest({
method: 'GET',
url: searchUrl,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
let hasResource = false;
if (data.Items && data.Items.length > 0) {
const chineseMatch = data.Items.find(item => {
const itemName = item.Name;
const itemYear = item.ProductionYear;
return itemName === name && itemYear === year;
});
if (chineseMatch) {
hasResource = true;
} else {
const englishMatch = data.Items.find(item => {
const itemOriginalTitle = item.OriginalTitle;
const itemYear = item.ProductionYear;
return itemOriginalTitle && itemOriginalTitle === name && itemYear === year;
});
hasResource = !!englishMatch;
}
}
state.embyCache.set(cacheKey, hasResource);
resolve(hasResource);
} catch (error) {
state.embyCache.set(cacheKey, false);
resolve(false);
}
},
onerror: function(error) {
state.embyCache.set(cacheKey, false);
resolve(false);
}
});
});
},
extractInfoFromPoster: (poster) => {
const nameElement = poster.querySelector('p[class*="MuiTypography-body1"][class*="mui-"]');
const yearElement = Array.from(poster.querySelectorAll('p[class*="MuiTypography-body1"][class*="mui-"]')).find(p => /^\d{4}$/.test(p.textContent.trim()));
if (nameElement && yearElement && nameElement !== yearElement) {
const name = nameElement.textContent.trim();
const year = parseInt(yearElement.textContent.trim(), 10);
if (name && !isNaN(year)) {
return { name, year, element: poster };
}
}
return null;
},
extractInfoFromDetail: () => {
const titleElement = document.querySelector('h1[class*="MuiTypography-h1"]');
if (titleElement) {
const name = titleElement.childNodes[0]?.textContent?.trim() ||
titleElement.textContent.replace(/\(\d{4}\).*$/, '').trim();
const yearSpan = titleElement.querySelector('span[class*="MuiTypography"]');
let year = null;
if (yearSpan) {
const yearMatch = yearSpan.textContent.match(/\((\d{4})\)/);
if (yearMatch) {
year = parseInt(yearMatch[1], 10);
}
}
if (!year) {
const directMatch = titleElement.textContent.match(/\((\d{4})\)/);
if (directMatch) {
year = parseInt(directMatch[1], 10);
}
}
if (name && year) {
return { name, year };
}
}
return null;
},
extractInfoFromUserPage: (element) => {
const text = element.textContent.trim();
const match = text.match(/(.+?)\s*\((\d{4})\)/);
if (match) {
const name = match[1].trim();
const year = parseInt(match[2], 10);
return { name, year, element };
}
return null;
},
extractInfoFromSearchYear: (element) => {
const text = element.textContent.trim();
const match = text.match(/\((\d{4})\)/);
if (match) {
const year = parseInt(match[1], 10);
const nameElement = element.previousElementSibling;
if (nameElement) {
const name = nameElement.textContent.trim();
return { name, year, element };
}
}
return null;
},
extractInfoFromCollection: (element) => {
const nameElement = element.querySelector('p[class*="MuiTypography-body1"][class*="mui-"]');
const yearElement = Array.from(element.querySelectorAll('p[class*="MuiTypography-body1"][class*="mui-"]')).find(p => /^\d{4}$/.test(p.textContent.trim()));
if (nameElement && yearElement && nameElement !== yearElement) {
const name = nameElement.textContent.trim();
const year = parseInt(yearElement.textContent.trim(), 10);
if (name && !isNaN(year)) {
return { name, year, element };
}
}
return null;
},
createPosterButton: (hasResource) => {
const btn = document.createElement('div');
btn.className = `emby-poster-btn ${hasResource ? 'has' : 'not-has'}`;
btn.textContent = hasResource ? '✓' : '✗';
btn.title = hasResource ? '已入库' : '未入库';
return btn;
},
createNameButton: (hasResource) => {
const btn = document.createElement('span');
btn.className = `emby-name-btn ${hasResource ? 'has' : 'not-has'}`;
const state = hasResource ? BUTTON_STYLES.nameBtn.has : BUTTON_STYLES.nameBtn.notHas;
btn.textContent = state.text;
btn.title = hasResource ? '已入库' : '未入库';
return btn;
},
createDetailPosterButton: (hasResource) => {
const btn = document.createElement('div');
btn.className = `emby-detail-poster-btn ${hasResource ? 'has' : 'not-has'}`;
btn.textContent = hasResource ? '✓' : '✗';
btn.title = hasResource ? '已入库' : '未入库';
return btn;
},
createDetailTitleButton: (hasResource) => {
const btn = document.createElement('span');
btn.className = `emby-detail-title-btn ${hasResource ? 'has' : 'not-has'}`;
const state = hasResource ? BUTTON_STYLES.detailBtn.titleBtn.has : BUTTON_STYLES.detailBtn.titleBtn.notHas;
btn.textContent = state.text;
btn.title = hasResource ? '已入库' : '未入库';
return btn;
},
createSearchYearButton: (hasResource) => {
const btn = document.createElement('span');
btn.className = `emby-search-year-btn ${hasResource ? 'has' : 'not-has'}`;
const state = hasResource ? BUTTON_STYLES.searchYearBtn.has : BUTTON_STYLES.searchYearBtn.notHas;
btn.textContent = state.text;
btn.title = hasResource ? '已入库' : '未入库';
return btn;
},
createUserPageButton: (hasResource) => {
const btn = document.createElement('span');
btn.className = `emby-user-page-btn ${hasResource ? 'has' : 'not-has'}`;
const state = hasResource ? BUTTON_STYLES.userPageBtn.has : BUTTON_STYLES.userPageBtn.notHas;
btn.textContent = state.text;
btn.title = hasResource ? 'Emby库中有此资源' : 'Emby库中无此资源';
btn.disabled = true;
return btn;
},
createCollectionButton: (hasResource) => {
const btn = document.createElement('span');
btn.className = `emby-collection-btn ${hasResource ? 'has' : 'not-has'}`;
const state = hasResource ? BUTTON_STYLES.collectionBtn.has : BUTTON_STYLES.collectionBtn.notHas;
btn.textContent = state.text;
btn.title = hasResource ? '已入库' : '未入库';
return btn;
},
createSettingButton: () => {
const btn = document.createElement('span');
btn.className = 'emby-setting-btn';
btn.innerHTML = `
<span style="display:inline-block;width:16px;height:16px;margin-right:8px;
background-image:url('https://raw.githubusercontent.com/lige47/QuanX-icon-rule/main/icon/04ProxySoft/emby.png');
background-size:contain;background-repeat:no-repeat;background-position:center;
filter:brightness(0.9);"></span>
<span>设置</span>
<span style="display:inline-block;width:16px;height:16px;margin-left:8px;
background-image:url('https://115.com/favicon.ico');
background-size:contain;background-repeat:no-repeat;background-position:center;"></span>
`;
btn.title = '多功能设置';
return btn;
},
createTransferButton: () => {
const btn = document.createElement('div');
btn.className = 'one-click-transfer-btn';
btn.style.cssText = 'cursor:pointer;margin-left:auto;background:rgba(128,128,128,0.15);color:#0d47a1;display:inline-flex;align-items:center;padding:0 12px;height:32px;border-radius:16px;font-weight:bold;font-size:14px;';
btn.innerHTML = '<img src="https://115.com/favicon.ico" style="width:16px;height:16px;margin-right:6px;">一键转存';
return btn;
}
};
const SettingsManager = {
showSettingsModal: () => {
if (document.querySelector('#tm-settings-modal')) return;
const embyHost = EMBY_CONFIG.HOST || '';
const embyApiKey = EMBY_CONFIG.API_KEY || '';
const cookie115 = GM_getValue('115_cookie') || '';
const cid115 = GM_getValue('115_cid') || '0';
const transferMethod = GM_getValue('115_transfer_method', 'cookie');
const symediaUrl = GM_getValue('symedia_url', '');
const symediaToken = GM_getValue('symedia_token', 'symedia');
const enableTransfer = GM_getValue('115_enable_transfer', true);
const overlay = document.createElement('div');
overlay.id = 'tm-settings-modal';
Object.assign(overlay.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: 'rgba(0, 0, 0, 0.3)',
backdropFilter: 'blur(12px) saturate(150%)',
WebkitBackdropFilter: 'blur(12px) saturate(150%)',
zIndex: 10001,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
animation: 'tmOverlayIn 0.25s ease'
});
const modal = document.createElement('div');
Object.assign(modal.style, {
background: 'rgba(255, 255, 255, 0.58)',
backdropFilter: 'blur(40px) saturate(200%)',
WebkitBackdropFilter: 'blur(40px) saturate(200%)',
padding: '0',
borderRadius: '24px',
width: '520px',
boxShadow: '0 24px 80px rgba(0, 0, 0, 0.08), 0 8px 32px rgba(0, 0, 0, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.8), inset 0 -1px 0 rgba(255, 255, 255, 0.3)',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif',
height: '85vh',
overflow: 'hidden',
border: '1px solid rgba(255, 255, 255, 0.65)',
animation: 'tmModalIn 0.35s cubic-bezier(0.16, 1, 0.3, 1)',
display: 'flex',
flexDirection: 'column'
});
modal.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;padding:28px 32px 16px 32px;border-bottom:1px solid rgba(0,0,0,0.06);flex-shrink:0;">
<h3 style="margin:0;font-size:17px;font-weight:700;color:#1a1a2e;letter-spacing:-0.3px;">HDHive</h3>
<button id="tm-settings-close" class="tm-close-btn" style="width:30px;height:30px;border-radius:50%;border:none;background:rgba(0,0,0,0.05);color:#888;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s ease;line-height:1;">×</button>
</div>
<div style="display:flex;gap:6px;padding:16px 32px 0 32px;flex-shrink:0;"><div style="display:flex;gap:6px;padding:4px;background:rgba(0,0,0,0.08);border-radius:14px;width:100%;">
<button class="tm-tab-btn active" data-tab="emby">Emby设置</button>
<button class="tm-tab-btn" data-tab="115">115网盘设置</button>
<button class="tm-tab-btn" data-tab="logs">日志</button>
</div></div>
<div style="flex:1;min-height:0;overflow-y:auto;padding:22px 32px 22px 32px;">
<div id="tm-tab-emby" class="tm-tab-content active">
<div style="margin-bottom:20px;">
<h4 style="margin-top:0;margin-bottom:10px;color:#444;">Emby服务器设置</h4>
<div style="margin-bottom:10px;">
<label style="display:block;margin-bottom:5px;color:#555;font-weight:bold;font-size:13px;">Emby地址:</label>
<input id="tm-emby-host" type="text" value="${embyHost}" placeholder="http/s://emby地址" style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:12px;">
</div>
<div style="margin-bottom:10px;">
<label style="display:block;margin-bottom:5px;color:#555;font-weight:bold;font-size:13px;">API密钥:</label>
<div style="display:flex;align-items:center;gap:8px;">
<input id="tm-emby-apikey" type="password" value="${embyApiKey}" placeholder="输入您的Emby API密钥" style="flex:1;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:12px;">
<button id="tm-toggle-emby-apikey" style="padding:6px 10px;border:none;border-radius:4px;background:#666;color:#fff;cursor:pointer;white-space:nowrap;font-size:12px;">显示</button>
</div>
</div>
<div style="margin-top:15px;padding-top:10px;border-top:1px dashed #eee;">
<button id="tm-emby-refresh" style="width:100%;padding:8px;border:none;border-radius:4px;background:#2196F3;color:#fff;cursor:pointer;font-size:12px;font-weight:bold;">🔄 刷新 Emby 缓存并重新扫描</button>
</div>
</div>
</div>
<div id="tm-tab-115" class="tm-tab-content">
<div style="margin-bottom:20px;">
<h4 style="margin-top:0;margin-bottom:10px;color:#444;">115网盘设置</h4>
<div style="display:flex;align-items:center;margin-bottom:10px;">
<input type="checkbox" id="tm-enable-transfer" ${enableTransfer ? 'checked' : ''} style="margin-right:8px;">
<label for="tm-enable-transfer" style="color:#555;font-weight:bold;font-size:13px;">启用115转存功能</label>
</div>
<div style="margin-bottom:15px;">
<label style="display:block;margin-bottom:8px;color:#555;font-weight:bold;font-size:13px;">转存方式:</label>
<input type="hidden" id="tm-transfer-method" value="${transferMethod}">
<div class="tm-custom-select" id="tm-custom-select">
<div class="tm-custom-select-trigger" id="tm-select-trigger">
<span class="tm-select-value">${transferMethod === 'cookie' ? '🍪 115 Cookie转存' : '🔗 Symedia API转存'}</span>
<span class="tm-select-arrow">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
<div class="tm-custom-select-dropdown" id="tm-select-dropdown">
<div class="tm-select-option ${transferMethod === 'cookie' ? 'selected' : ''}" data-value="cookie">
<span>🍪 115 Cookie转存</span>
<span class="tm-select-check">${transferMethod === 'cookie' ? '✓' : ''}</span>
</div>
<div class="tm-select-option ${transferMethod === 'symedia' ? 'selected' : ''}" data-value="symedia">
<span>🔗 Symedia API转存</span>
<span class="tm-select-check">${transferMethod === 'symedia' ? '✓' : ''}</span>
</div>
</div>
</div>
</div>
<div id="tm-cookie-settings" class="tm-transfer-settings" style="display:${transferMethod === 'cookie' ? 'block' : 'none'};">
<div style="margin-bottom:10px;">
<label style="display:block;margin-bottom:5px;color:#555;font-weight:bold;font-size:13px;">Cookie:</label>
<div style="display:flex;align-items:center;gap:8px;">
<input id="tm-cookie-input" type="password" value="${cookie115}" placeholder="请输入115 Cookie" style="flex:1;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:12px;">
<button id="tm-toggle-cookie" style="padding:6px 10px;border:none;border-radius:4px;background:#666;color:#fff;cursor:pointer;white-space:nowrap;font-size:12px;">显示</button>
</div>
<div style="font-size:11px;color:#666;margin-top:4px;">
格式:UID=xxx;CID=xxx;SEID=xxx;... 或直接从浏览器复制
</div>
</div>
</div>
<div id="tm-symedia-settings" class="tm-transfer-settings" style="display:${transferMethod === 'symedia' ? 'block' : 'none'};">
<div style="margin-bottom:10px;">
<label style="display:block;margin-bottom:5px;color:#555;font-weight:bold;font-size:13px;">Symedia地址:</label>
<input id="tm-symedia-url" type="text" value="${symediaUrl}" placeholder="http://127.0.0.1:8095" style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:12px;">
</div>
<div style="margin-bottom:10px;">
<label style="display:block;margin-bottom:5px;color:#555;font-weight:bold;font-size:13px;">Token:</label>
<input id="tm-symedia-token" type="text" value="${symediaToken}" placeholder="默认: symedia" style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:12px;">
</div>
<div style="font-size:11px;color:#666;margin-top:4px;">
格式:http://IP:端口,token默认symedia
</div>
</div>
<div style="margin-bottom:10px;">
<label style="display:block;margin-bottom:5px;color:#555;font-weight:bold;font-size:13px;">目标文件夹CID:</label>
<div style="display:flex;gap:10px;">
<input id="tm-cid-input" type="text" value="${cid115}" placeholder="0为根目录" style="flex:1;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:12px;">
<button id="tm-browse-folders" style="padding:6px 12px;border:none;border-radius:4px;background:#2196F3;color:#fff;cursor:pointer;font-size:12px;">浏览文件夹</button>
</div>
<div style="font-size:11px;color:#666;margin-top:4px;">
<div>说明:</div>
<div>1. 如果关闭"启用115转存功能",则不会执行转存,只输出链接和日志</div>
<div>2. Cookie转存:需要有效的115 Cookie,可点击"浏览文件夹"选择</div>
<div>3. Symedia转存:转存可触发实时监控归档整理 token默认symedia</div>
<div>4. 文件夹ID为0时转存到根目录</div>
<div>5. 当前设置: ${cid115}</div>
</div>
</div>
</div>
</div>
<div id="tm-tab-logs" class="tm-tab-content">
<div style="margin-bottom:20px;">
<h4 style="margin-top:0;margin-bottom:10px;color:#444;">操作日志</h4>
<div style="margin-bottom:10px;display:flex;gap:10px;">
<button id="tm-clear-logs" style="padding:6px 12px;border:none;border-radius:4px;background:#f44336;color:#fff;cursor:pointer;font-size:12px;">清空日志</button>
<button id="tm-show-log-panel" style="padding:6px 12px;border:none;border-radius:4px;background:#2196F3;color:#fff;cursor:pointer;font-size:12px;">显示独立日志面板</button>
</div>
<div style="border:1px solid rgba(0,0,0,0.05);border-radius:14px;padding:14px;height:300px;overflow-y:auto;background:rgba(255,255,255,0.35);font-size:11px;backdrop-filter:blur(4px);">
<div id="tm-settings-log-content"></div>
</div>
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;padding:18px 32px 28px 32px;border-top:1px solid rgba(0,0,0,0.06);flex-shrink:0;">
<button id="tm-settings-cancel" class="tm-btn-cancel" style="padding:10px 24px;border:1px solid rgba(0,0,0,0.08);border-radius:12px;background:rgba(255,255,255,0.5);color:#666;cursor:pointer;font-size:13px;font-weight:500;backdrop-filter:blur(4px);">取消</button>
<button id="tm-settings-save" class="tm-btn-save" style="padding:10px 24px;border:none;border-radius:12px;background:linear-gradient(135deg,#4CAF50,#2E7D32);color:#fff;cursor:pointer;font-size:13px;font-weight:600;box-shadow:0 4px 16px rgba(76,175,80,0.25);">保存设置</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const tabBtns = modal.querySelectorAll('.tm-tab-btn');
const tabContents = modal.querySelectorAll('.tm-tab-content');
tabBtns.forEach(btn => {
btn.addEventListener('click', function() {
const tab = this.getAttribute('data-tab');
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
this.classList.add('active');
document.getElementById(`tm-tab-${tab}`).classList.add('active');
if (tab === 'logs') SettingsManager.refreshLogContent();
});
});
const transferMethodHidden = modal.querySelector('#tm-transfer-method');
const cookieSettings = modal.querySelector('#tm-cookie-settings');
const symediaSettings = modal.querySelector('#tm-symedia-settings');
const browseFoldersBtn = modal.querySelector('#tm-browse-folders');
const customSelect = modal.querySelector('#tm-custom-select');
const selectTrigger = modal.querySelector('#tm-select-trigger');
const selectDropdown = modal.querySelector('#tm-select-dropdown');
const selectOptions = modal.querySelectorAll('.tm-select-option');
const selectValue = selectTrigger.querySelector('.tm-select-value');
selectTrigger.addEventListener('click', function(e) {
e.stopPropagation();
customSelect.classList.toggle('open');
});
selectOptions.forEach(opt => {
opt.addEventListener('click', function(e) {
e.stopPropagation();
const method = this.getAttribute('data-value');
transferMethodHidden.value = method;
selectOptions.forEach(o => {
o.classList.remove('selected');
o.querySelector('.tm-select-check').textContent = '';
});
this.classList.add('selected');
this.querySelector('.tm-select-check').textContent = '✓';
selectValue.textContent = this.querySelector('span').textContent;
customSelect.classList.remove('open');
cookieSettings.style.display = method === 'cookie' ? 'block' : 'none';
symediaSettings.style.display = method === 'symedia' ? 'block' : 'none';
browseFoldersBtn.style.display = method === 'cookie' ? '' : 'none';
});
});
document.addEventListener('click', function closeSelect(e) {
if (!customSelect.contains(e.target)) {
customSelect.classList.remove('open');
}
});
modal.querySelector('#tm-settings-close').onclick = () => overlay.remove();
const embyApiKeyInput = modal.querySelector('#tm-emby-apikey');
const toggleEmbyApiKeyBtn = modal.querySelector('#tm-toggle-emby-apikey');
if (toggleEmbyApiKeyBtn) {
toggleEmbyApiKeyBtn.addEventListener('click', function() {
if (embyApiKeyInput.type === 'password') {
embyApiKeyInput.type = 'text';
toggleEmbyApiKeyBtn.textContent = '隐藏';
} else {
embyApiKeyInput.type = 'password';
toggleEmbyApiKeyBtn.textContent = '显示';
}
});
}
modal.querySelector('#tm-emby-refresh').onclick = async () => {
const btn = modal.querySelector('#tm-emby-refresh');
const originalText = btn.innerHTML;
btn.innerHTML = '🔄 正在刷新...';
btn.disabled = true;
state.embyCache.clear();
state.processedItems.clear();
state.processingItems.clear();
const selectorList = [
'.emby-poster-btn',
'.emby-name-btn',
'.emby-detail-poster-btn',
'.emby-detail-title-btn',
'.emby-search-year-btn',
'.emby-user-page-btn',
'.emby-collection-btn'
];
document.querySelectorAll(selectorList.join(',')).forEach(el => el.remove());
await processAllPosters();
setTimeout(() => {
btn.innerHTML = '✅ 刷新完成';
setTimeout(() => {
btn.innerHTML = originalText;
btn.disabled = false;
}, 1000);
}, 500);
};
const cookieInput = modal.querySelector('#tm-cookie-input');
const toggleCookieBtn = modal.querySelector('#tm-toggle-cookie');
toggleCookieBtn.addEventListener('click', function() {
if (cookieInput.type === 'password') {
cookieInput.type = 'text';
toggleCookieBtn.textContent = '隐藏';
} else {
cookieInput.type = 'password';
toggleCookieBtn.textContent = '显示';
}
});
modal.querySelector('#tm-browse-folders').onclick = () => {
const cookieValue = cookieInput.value.trim();
if (!cookieValue) {
alert('请先输入Cookie');
return;
}
GM_setValue('115_cookie', cookieValue);
SettingsManager.showFolderBrowser();
};
modal.querySelector('#tm-clear-logs').onclick = () => {
if (Logger.logContent) {
Logger.logContent.innerHTML = '';
Logger.stats = { free: 0, paid: 0, unlocked: 0 };
document.querySelectorAll('#log-stats span').forEach(span => {
span.style.display = 'none';
span.textContent = span.id.replace('stat-', '') + ': 0';
});
}
SettingsManager.refreshLogContent();
};
modal.querySelector('#tm-show-log-panel').onclick = () => {
Logger.showLogPanel();
overlay.remove();
};
modal.querySelector('#tm-settings-cancel').onclick = () => overlay.remove();
modal.querySelector('#tm-settings-save').onclick = () => {
const newEmbyHost = modal.querySelector('#tm-emby-host').value.trim();
const newEmbyApiKey = embyApiKeyInput.value.trim();
const newCookie = cookieInput.value.trim();
const newCid = modal.querySelector('#tm-cid-input').value.trim() || '0';
const newTransferMethod = modal.querySelector('#tm-transfer-method').value;
const newSymediaUrl = modal.querySelector('#tm-symedia-url').value.trim();
const newSymediaToken = modal.querySelector('#tm-symedia-token').value.trim() || 'symedia';
const newEnableTransfer = modal.querySelector('#tm-enable-transfer').checked;
GM_setValue('embyHost', newEmbyHost);
GM_setValue('embyApiKey', newEmbyApiKey);
EMBY_CONFIG.HOST = newEmbyHost;
EMBY_CONFIG.API_KEY = newEmbyApiKey;
GM_setValue('115_cookie', newCookie);
GM_setValue('115_cid', newCid);
GM_setValue('115_transfer_method', newTransferMethod);
GM_setValue('symedia_url', newSymediaUrl);
GM_setValue('symedia_token', newSymediaToken);
GM_setValue('115_enable_transfer', newEnableTransfer);
state.embyCache.clear();
state.processedItems.clear();
state.processingItems.clear();
state.transferButtonsInitialized = false;
Logger.addLog('✅ 所有设置已保存', 'success');
overlay.remove();
processAllPosters();
if (Utils.isUserPage()) {
processUserPageButtons();
} else if (Utils.isParentPage()) {
addButtons();
}
};
SettingsManager.refreshLogContent();
const style = document.createElement('style');
style.textContent = `
@keyframes tmOverlayIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes tmModalIn {
from { opacity: 0; transform: scale(0.96) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes tmTabFade {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.tm-tab-btn {
padding: 8px 16px;
border: none;
background: rgba(0, 0, 0, 0.04);
color: #333;
cursor: pointer;
font-size: 13px;
border-radius: 10px;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
font-weight: 600;
}
.tm-tab-btn:hover {
background: rgba(255, 255, 255, 0.7);
color: #111;
}
.tm-tab-btn.active {
background: rgba(255, 255, 255, 0.9);
color: #1565C0;
font-weight: 700;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.06);
}
.tm-tab-content { display: none; }
.tm-tab-content.active { display: block; animation: tmTabFade 0.3s ease; }
.tm-tab-content > div {
background: rgba(255, 255, 255, 0.3);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
}
#tm-settings-modal input[type="text"],
#tm-settings-modal input[type="password"],
#tm-settings-modal select {
background: rgba(255, 255, 255, 0.45) !important;
border: 1px solid rgba(0, 0, 0, 0.07) !important;
border-radius: 12px !important;
padding: 10px 14px !important;
transition: all 0.25s ease;
font-size: 13px !important;
color: #333;
box-sizing: border-box;
}
#tm-settings-modal input[type="text"]:focus,
#tm-settings-modal input[type="password"]:focus,
#tm-settings-modal select:focus {
background: rgba(255, 255, 255, 0.8) !important;
border-color: rgba(33, 150, 243, 0.35) !important;
box-shadow: 0 0 0 4px rgba(33, 150, 243, 0.08), 0 2px 8px rgba(33, 150, 243, 0.06);
outline: none;
}
#tm-settings-modal input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #2196F3;
cursor: pointer;
}
#tm-settings-modal button {
border-radius: 10px !important;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
font-weight: 500;
}
#tm-settings-modal button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
#tm-settings-modal button:active {
transform: translateY(0) scale(0.98);
transition: all 0.1s ease;
}
.tm-close-btn:hover {
background: rgba(0, 0, 0, 0.1) !important;
color: #333 !important;
transform: scale(1.08) !important;
box-shadow: none !important;
}
.tm-btn-cancel:hover {
background: rgba(255, 255, 255, 0.8) !important;
border-color: rgba(0, 0, 0, 0.12) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
}
.tm-btn-save:hover {
box-shadow: 0 8px 24px rgba(76, 175, 80, 0.35) !important;
transform: translateY(-2px) !important;
}
#tm-settings-modal h4 {
color: #1a1a2e !important;
font-weight: 600;
font-size: 14px;
padding-left: 12px;
border-left: 3px solid #2196F3;
margin-top: 0;
margin-bottom: 14px;
line-height: 1.4;
}
#tm-settings-modal label {
color: #444 !important;
font-size: 13px;
}
#tm-settings-modal ::-webkit-scrollbar {
width: 5px;
}
#tm-settings-modal ::-webkit-scrollbar-track {
background: transparent;
}
#tm-settings-modal ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
#tm-settings-modal ::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.2);
}
.tm-custom-select {
position: relative;
width: 100%;
user-select: none;
}
.tm-custom-select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: rgba(255, 255, 255, 0.45);
border: 1px solid rgba(0, 0, 0, 0.07);
border-radius: 12px;
cursor: pointer;
transition: all 0.25s ease;
font-size: 13px;
color: #333;
}
.tm-custom-select-trigger:hover {
background: rgba(255, 255, 255, 0.65);
border-color: rgba(0, 0, 0, 0.12);
}
.tm-custom-select.open .tm-custom-select-trigger {
background: rgba(255, 255, 255, 0.8);
border-color: rgba(33, 150, 243, 0.35);
box-shadow: 0 0 0 4px rgba(33, 150, 243, 0.08), 0 2px 8px rgba(33, 150, 243, 0.06);
}
.tm-select-arrow {
display: flex;
align-items: center;
color: #999;
transition: transform 0.25s ease;
}
.tm-custom-select.open .tm-select-arrow {
transform: rotate(180deg);
color: #1565C0;
}
.tm-custom-select-dropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
z-index: 100;
opacity: 0;
visibility: hidden;
transform: translateY(-8px) scale(0.98);
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: none;
}
.tm-custom-select.open .tm-custom-select-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.tm-select-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
font-size: 13px;
color: #444;
cursor: pointer;
transition: background 0.15s ease;
}
.tm-select-option:hover {
background: rgba(33, 150, 243, 0.06);
}
.tm-select-option.selected {
color: #1565C0;
font-weight: 600;
}
.tm-select-option + .tm-select-option {
border-top: 1px solid rgba(0, 0, 0, 0.04);
}
.tm-select-check {
color: #1565C0;
font-weight: 700;
font-size: 14px;
}
`;
modal.appendChild(style);
},
refreshLogContent: () => {
const logContainer = document.querySelector('#tm-settings-log-content');
if (!logContainer || !Logger.logContent) return;
logContainer.innerHTML = Logger.logContent.innerHTML || '<div style="text-align:center;color:#999;padding:20px;">暂无日志</div>';
},
showFolderBrowser: async () => {
if (document.querySelector('#tm-folder-browser')) return;
const overlay = document.createElement('div');
overlay.id = 'tm-folder-browser';
Object.assign(overlay.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.5)',
zIndex: 10002,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
});
const modal = document.createElement('div');
Object.assign(modal.style, {
background: '#fff',
padding: '20px',
borderRadius: '10px',
width: '500px',
maxHeight: '80vh',
boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
fontFamily: 'Arial, sans-serif',
display: 'flex',
flexDirection: 'column'
});
modal.innerHTML = `
<h3 style="margin-top:0;margin-bottom:15px;color:#333">浏览文件夹</h3>
<div id="tm-current-path" style="margin-bottom:10px;padding:8px;background:#f5f5f5;border-radius:4px;font-size:12px;">根目录</div>
<div id="tm-folders-list" style="flex:1;overflow-y:auto;margin-bottom:15px;min-height:200px;border:1px solid #eee;border-radius:4px;">
<div style="text-align:center;padding:40px 0;color:#999;">加载中...</div>
</div>
<div style="display:flex;justify-content:space-between;gap:10px;">
<button id="tm-folder-back" style="flex:1;padding:8px 12px;border:none;border-radius:4px;background:#ccc;color:#fff;cursor:pointer;font-size:12px;">返回上级</button>
<button id="tm-folder-cancel" style="flex:1;padding:8px 12px;border:none;border-radius:4px;background:#ccc;color:#fff;cursor:pointer;font-size:12px;">取消</button>
<button id="tm-folder-select" style="flex:1;padding:8px 12px;border:none;border-radius:4px;background:#4CAF50;color:#fff;cursor:pointer;font-size:12px;">选择</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
let currentCid = 0;
let currentPath = ["根目录"];
let cidStack = [];
let pathStack = [];
const getFolders = async (cid = 0) => {
const cookie = GM_getValue('115_cookie');
if (!cookie) {
Logger.addLog('❌ 获取文件夹失败:Cookie 未设置', 'error');
return [];
}
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://webapi.115.com/files?aid=1&cid=${cid}&show_dir=1&nsprefix=1`,
headers: {
"Cookie": cookie,
"User-Agent": "Mozilla/5.0"
},
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.state && data.data) {
const folders = data.data
.filter(item => item.fl && item.fl.length === 0)
.map(item => ({
name: item.n,
cid: item.cid
}));
resolve(folders);
} else {
resolve([]);
}
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([])
});
});
};
const loadFolders = async (cid = 0) => {
const foldersList = document.getElementById('tm-folders-list');
foldersList.innerHTML = '<div style="text-align:center;padding:40px 0;color:#999;">加载中...</div>';
const folders = await getFolders(cid);
if (folders.length === 0) {
foldersList.innerHTML = '<div style="text-align:center;padding:40px 0;color:#999;">该目录下没有文件夹</div>';
return;
}
foldersList.innerHTML = '';
folders.forEach(folder => {
const folderItem = document.createElement('div');
Object.assign(folderItem.style, {
padding: '10px',
borderBottom: '1px solid #eee',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
fontSize: '12px'
});
folderItem.innerHTML = `
<span>${folder.name}</span>
<span style="color:#999;">CID: ${folder.cid}</span>
`;
folderItem.addEventListener('mouseenter', () => {
folderItem.style.backgroundColor = '#f5f5f5';
});
folderItem.addEventListener('mouseleave', () => {
folderItem.style.backgroundColor = 'transparent';
});
folderItem.onclick = () => {
cidStack.push(currentCid);
pathStack.push([...currentPath]);
currentCid = folder.cid;
currentPath.push(folder.name);
updatePathDisplay();
loadFolders(currentCid);
};
foldersList.appendChild(folderItem);
});
};
const updatePathDisplay = () => {
const pathElement = document.getElementById('tm-current-path');
pathElement.textContent = currentPath.join(' / ');
};
modal.querySelector('#tm-folder-back').onclick = () => {
if (cidStack.length > 0) {
currentCid = cidStack.pop();
currentPath = pathStack.pop();
updatePathDisplay();
loadFolders(currentCid);
}
};
modal.querySelector('#tm-folder-cancel').onclick = () => {
overlay.remove();
};
modal.querySelector('#tm-folder-select').onclick = () => {
if (currentCid !== 0) {
const cidInput = document.querySelector('#tm-cid-input');
if (cidInput) {
cidInput.value = currentCid;
}
Logger.addLog(`已选择文件夹: ${currentPath.join(' / ')} (CID: ${currentCid})`, 'success');
}
overlay.remove();
};
await loadFolders(currentCid);
updatePathDisplay();
},
addSettingButton: () => {
const checkInterval = setInterval(() => {
const titleEl = Array.from(document.querySelectorAll('span[class*="MuiBox-root"][class*="mui-"]'))
.find(el => el.textContent && el.textContent.includes('HDHIVE'));
if (titleEl) {
clearInterval(checkInterval);
if (document.querySelector('.emby-setting-btn')) return;
const btn = EmbyHelper.createSettingButton();
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
SettingsManager.showSettingsModal();
});
titleEl.parentNode.insertBefore(btn, titleEl.nextSibling);
}
}, 500);
}
};
async function processPoster(poster) {
const itemKey = `poster-${poster.href}`;
if (state.processedItems.has(itemKey) || state.processingItems.has(itemKey)) {
return;
}
state.processingItems.add(itemKey);
const info = EmbyHelper.extractInfoFromPoster(poster);
if (!info) {
state.processingItems.delete(itemKey);
return;
}
try {
const hasResource = await EmbyHelper.checkEmbyResource(info.name, info.year);
if (!poster.querySelector('.emby-poster-btn')) {
const posterImageContainer = poster.querySelector('div[class*="Box-root"]');
if (posterImageContainer) {
const btn = EmbyHelper.createPosterButton(hasResource);
posterImageContainer.style.position = 'relative';
posterImageContainer.appendChild(btn);
}
}
if (!poster.querySelector('.emby-name-btn') && !poster.querySelector('.emby-collection-btn') && !Utils.isCollectionPage()) {
const yearElement = info.element.querySelector('p[class*="MuiTypography-body1"][class*="mui-"]');
if (yearElement) {
const btn = EmbyHelper.createNameButton(hasResource);
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'center';
buttonContainer.style.width = '100%';
buttonContainer.appendChild(btn);
yearElement.parentNode.insertBefore(buttonContainer, yearElement.nextSibling);
}
}
state.processedItems.add(itemKey);
} catch (error) {
} finally {
state.processingItems.delete(itemKey);
}
}
async function processDetailPage() {
if (!Utils.isDetailPage()) return;
const detailKey = 'detail-page';
if (state.processedItems.has(detailKey) || state.processingItems.has(detailKey)) {
return;
}
state.processingItems.add(detailKey);
const info = EmbyHelper.extractInfoFromDetail();
if (!info) {
state.processingItems.delete(detailKey);
return;
}
try {
const hasResource = await EmbyHelper.checkEmbyResource(info.name, info.year);
if (!document.querySelector('.emby-detail-poster-btn')) {
const posterImage = document.querySelector('img.poster-item, img[alt*="海报"], img[class*="poster"]');
let posterContainer = null;
if (posterImage) {
posterContainer = posterImage.closest('div[class*="Box-root"]');
}
if (!posterContainer) {
posterContainer = document.querySelector('div[class*="Box-root"]');
}
if (posterContainer) {
const btn = EmbyHelper.createDetailPosterButton(hasResource);
posterContainer.style.position = 'relative';
posterContainer.appendChild(btn);
}
}
if (!document.querySelector('.emby-detail-title-btn')) {
const titleElement = document.querySelector('h1');
if (titleElement) {
const btn = EmbyHelper.createDetailTitleButton(hasResource);
titleElement.parentNode.insertBefore(btn, titleElement.nextSibling);
}
}
state.processedItems.add(detailKey);
} catch (error) {
} finally {
state.processingItems.delete(detailKey);
}
}
async function processSearchYearButtons() {
const resultItems = document.querySelectorAll('a[href*="/tmdb/"]');
for (const item of resultItems) {
const itemKey = `search-${item.href}`;
if (state.processedItems.has(itemKey) || state.processingItems.has(itemKey)) {
continue;
}
state.processingItems.add(itemKey);
const yearText = item.querySelector('[class*="MuiTypography-body2"][class*="mui-"]');
if (yearText && yearText.textContent.includes('(')) {
const info = EmbyHelper.extractInfoFromSearchYear(yearText);
if (info) {
try {
const hasResource = await EmbyHelper.checkEmbyResource(info.name, info.year);
if (!yearText.parentNode.querySelector('.emby-search-year-btn')) {
const btn = EmbyHelper.createSearchYearButton(hasResource);
yearText.parentNode.insertBefore(btn, yearText.nextSibling);
}
} catch (error) {
}
}
}
state.processedItems.add(itemKey);
state.processingItems.delete(itemKey);
}
}
async function processCollectionButtons() {
if (!Utils.isCollectionPage()) return;
const collectionItems = document.querySelectorAll('a[href*="/tmdb/"]');
for (const item of collectionItems) {
const itemKey = `collection-${item.href}`;
if (state.processedItems.has(itemKey) || state.processingItems.has(itemKey)) {
continue;
}
state.processingItems.add(itemKey);
const info = EmbyHelper.extractInfoFromCollection(item);
if (info) {
try {
const hasResource = await EmbyHelper.checkEmbyResource(info.name, info.year);
if (!item.querySelector('.emby-poster-btn')) {
const posterImageContainer = item.querySelector('div[class*="Box-root"]');
if (posterImageContainer) {
const btn = EmbyHelper.createPosterButton(hasResource);
posterImageContainer.style.position = 'relative';
posterImageContainer.appendChild(btn);
}
}
if (!item.querySelector('.emby-collection-btn') && !item.querySelector('.emby-name-btn')) {
const btn = EmbyHelper.createCollectionButton(hasResource);
const yearElement = info.element.querySelector('p[class*="MuiTypography-body1"][class*="mui-"]');
if (yearElement) {
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'center';
buttonContainer.style.width = '100%';
buttonContainer.appendChild(btn);
yearElement.parentNode.insertBefore(buttonContainer, yearElement.nextSibling);
}
}
} catch (error) {
}
}
state.processedItems.add(itemKey);
state.processingItems.delete(itemKey);
}
const resource115Links = document.querySelectorAll('a[href*="/resource/115/"]');
resource115Links.forEach(link => {
const container = link.closest('a[class*="MuiBox-root"]');
if (!container || container.querySelector('.one-click-transfer-btn')) return;
let resourceType = 'unknown';
let cost = '';
const chips = container.querySelectorAll('[class*="MuiChip-root"][class*="mui-"]');
chips.forEach(chip => {
const chipText = chip.textContent.trim();
if (chipText.includes('免费')) {
resourceType = 'free';
} else if (chipText.includes('已解锁')) {
resourceType = 'unlocked';
} else if (chipText.includes('积分')) {
resourceType = 'paid';
const match = chipText.match(/(\d+)\s*积分/);
if (match) cost = match[1];
}
});
Logger.updateStats(resourceType);
const btn = EmbyHelper.createTransferButton();
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
let logType = '';
if (resourceType === 'paid' && cost) logType = `${cost}积分`;
else if (resourceType === 'free') logType = '免费';
else if (resourceType === 'unlocked') logType = '已解锁';
else logType = '未知类型';
const rawTarget = `${location.origin}${link.getAttribute('href')}`;
if (Logger.isLinkProcessed(rawTarget)) {
Logger.addLog(`该资源已在处理中,请勿重复点击`, 'process');
return;
}
Logger.markLinkAsProcessed(rawTarget);
Logger.startNewTask(rawTarget);
Logger.addLog(`开始处理链接 [${logType}] <a href="${rawTarget}" target="_blank">${rawTarget}</a>`,'process');
const finalType = (resourceType === 'free' || resourceType === 'unlocked') ? 'free' : (resourceType === 'paid' ? 'paid' : 'user_auto');
const processedTarget = `${rawTarget}?autotransfer=1&type=${finalType}`;
window.open(processedTarget, '_blank', `width=300,height=200,left=0,top=100,resizable=yes,scrollbars=yes`);
};
const descriptionElement = container.querySelector('p[class*="MuiTypography-root"][class*="MuiTypography-body2"][aria-label]');
if (descriptionElement) {
const descriptionContainer = descriptionElement.parentElement;
const buttonWrapper = document.createElement('div');
buttonWrapper.style.marginTop = '8px';
buttonWrapper.style.width = '100%';
buttonWrapper.style.display = 'flex';
buttonWrapper.style.justifyContent = 'flex-end';
buttonWrapper.appendChild(btn);
descriptionContainer.appendChild(buttonWrapper);
} else {
const topSection = container.querySelector('[class*="MuiStack-root"]');
if (topSection) {
const buttonWrapper = document.createElement('div');
buttonWrapper.style.marginTop = '8px';
buttonWrapper.style.width = '100%';
buttonWrapper.style.display = 'flex';
buttonWrapper.style.justifyContent = 'flex-end';
buttonWrapper.appendChild(btn);
topSection.parentNode.insertBefore(buttonWrapper, topSection.nextSibling);
}
}
});
}
async function processUserPageButtons() {
const elements = Array.from(document.querySelectorAll('h6[class*="MuiTypography-subtitle2"][class*="mui-"]'))
.filter(el => /\(\d{4}\)/.test(el.textContent));
const uniqueMovies = new Map();
for (const el of elements) {
const info = EmbyHelper.extractInfoFromUserPage(el);
if (info) {
const cacheKey = `${info.name}-${info.year}`;
if (!uniqueMovies.has(cacheKey)) {
uniqueMovies.set(cacheKey, {
name: info.name,
year: info.year,
elements: [],
containers: []
});
}
uniqueMovies.get(cacheKey).elements.push(el);
const container = el.closest('[class*="MuiGrid"][class*="mui-"]');
if (container) {
uniqueMovies.get(cacheKey).containers.push(container);
}
}
}
for (const [cacheKey, movie] of uniqueMovies) {
const itemKey = `user-${cacheKey}`;
if (state.processedItems.has(itemKey) || state.processingItems.has(itemKey)) {
continue;
}
state.processingItems.add(itemKey);
try {
const hasResource = await EmbyHelper.checkEmbyResource(movie.name, movie.year);
const containers115 = movie.containers.filter(container => {
return Utils.getDiskType(container) === '115';
});
const elements115 = containers115.map(container => {
return container.querySelector('h6[class*="MuiTypography-subtitle2"][class*="mui-"]');
}).filter(el => el);
let targetEl, targetContainer;
if (elements115.length > 0) {
const randomIndex = Math.floor(Math.random() * elements115.length);
targetEl = elements115[randomIndex];
targetContainer = containers115[randomIndex];
} else if (movie.elements.length > 0) {
const randomIndex = Math.floor(Math.random() * movie.elements.length);
targetEl = movie.elements[randomIndex];
targetContainer = movie.containers[randomIndex];
}
if (targetEl && !targetEl.querySelector('.emby-user-page-btn')) {
const btn = EmbyHelper.createUserPageButton(hasResource);
targetEl.appendChild(btn);
}
if (targetContainer) {
const imageContainers = targetContainer.querySelectorAll('div[class*="MuiBox-root"]');
let imageContainer = null;
for (const container of imageContainers) {
if (container.querySelector('img') || container.style.backgroundImage) {
imageContainer = container;
break;
}
}
if (imageContainer && !imageContainer.querySelector('.emby-poster-btn')) {
const posterBtn = EmbyHelper.createPosterButton(hasResource);
posterBtn.style.position = 'absolute';
posterBtn.style.top = '10px';
posterBtn.style.left = '10px';
posterBtn.style.right = 'auto';
posterBtn.style.zIndex = '10';
imageContainer.style.position = 'relative';
imageContainer.appendChild(posterBtn);
}
}
} catch (error) {
} finally {
state.processedItems.add(itemKey);
state.processingItems.delete(itemKey);
}
}
document.querySelectorAll('[class*="MuiGrid"][class*="mui-"]').forEach(container => {
if (container.querySelector('.one-click-transfer-btn')) return;
const link = container.querySelector('a[href*="/resource/"]');
if (!link) return;
const diskType = Utils.getDiskType(container);
if (diskType !== '115') return;
let resourceType = 'unknown';
let cost = '';
const chips = container.querySelectorAll('[class*="MuiChip-root"][class*="mui-"]');
chips.forEach(chip => {
const chipText = chip.textContent.trim();
if (chipText.includes('免费')) {
resourceType = 'free';
} else if (chipText.includes('已解锁')) {
resourceType = 'unlocked';
} else if (chipText.includes('积分')) {
resourceType = 'paid';
const match = chipText.match(/(\d+)\s*积分/);
if (match) cost = match[1];
}
});
const btn = EmbyHelper.createTransferButton();
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
let logType = '';
if (resourceType === 'paid' && cost) logType = `${cost}积分`;
else if (resourceType === 'free') logType = '免费';
else if (resourceType === 'unlocked') logType = '已解锁';
else logType = '未知类型';
const rawTarget = `${location.origin}${link.getAttribute('href')}`;
if (Logger.isLinkProcessed(rawTarget)) {
Logger.addLog(`⚠️ 该资源已在处理中,请勿重复点击`, 'process');
return;
}
Logger.markLinkAsProcessed(rawTarget);
Logger.startNewTask(rawTarget);
Logger.addLog(`开始处理链接 [${logType}] <a href="${rawTarget}" target="_blank">${rawTarget}</a>`,'process');
const finalType = (resourceType === 'free' || resourceType === 'unlocked') ? 'free' : (resourceType === 'paid' ? 'paid' : 'user_auto');
const processedTarget = `${rawTarget}?autotransfer=1&type=${finalType}`;
window.open(processedTarget, '_blank', `width=300,height=200,left=0,top=100,resizable=yes,scrollbars=yes`);
};
const descriptionElement = container.querySelector('p[class*="MuiTypography-root"][class*="MuiTypography-body2"]');
const buttonWrapper = document.createElement('div');
buttonWrapper.style.marginTop = '8px';
buttonWrapper.style.width = '100%';
buttonWrapper.style.display = 'flex';
buttonWrapper.style.justifyContent = 'flex-end';
buttonWrapper.appendChild(btn);
if (descriptionElement) {
descriptionElement.parentElement.appendChild(buttonWrapper);
} else {
const titleElement = container.querySelector('h6[class*="MuiTypography-root"]');
if (titleElement && titleElement.parentElement) {
titleElement.parentElement.appendChild(buttonWrapper);
}
}
});
}
async function processAllPosters() {
const posters = document.querySelectorAll('a[href*="/movie/"], a[href*="/tv/"]');
for (const poster of posters) {
await processPoster(poster);
}
const popoverPosters = document.querySelectorAll('a[href*="/movie/"], a[href*="/tv/"]');
for (const poster of popoverPosters) {
await processPoster(poster);
}
await processDetailPage();
await processSearchYearButtons();
await processCollectionButtons();
if (Utils.isUserPage()) {
FilterManager.injectBar();
await processUserPageButtons();
}
}
function removeButtons(){document.querySelectorAll('.one-click-transfer-btn, .detail-page-transfer-btn').forEach(b=>b.remove());}
function addButtons(){
const resourceContainers = document.querySelectorAll('[class*="MuiGrid"][class*="mui-"]');
resourceContainers.forEach((container)=>{
if(container.querySelector('.one-click-transfer-btn')) {
return;
}
const link=container.querySelector('a[href^="/resource/"]');
if(!link) {
return;
}
const is115 = link.href.includes('/resource/115/');
if (!is115) {
return;
}
let type='unknown'; let cost='';
const chips = container.querySelectorAll('[class*="MuiChip-root"][class*="mui-"]');
chips.forEach(chip=>{
const t=chip.textContent.trim();
if(t.includes('免费')) {
type='free';
} else if(t.includes('已解锁')) {
type='unlocked';
} else if(t.includes('积分')){
type='paid';
const match = t.match(/(\d+)\s*积分/);
if(match) cost = match[1];
}
});
Logger.updateStats(type);
const btn=EmbyHelper.createTransferButton();
btn.onclick=(e)=>{
e.preventDefault(); e.stopPropagation();
let logType = '';
if(type === 'paid' && cost) logType = `${cost}积分`;
else if(type === 'free') logType = '免费';
else if(type === 'unlocked') logType = '已解锁';
else logType = '未知类型';
const rawTarget=`${location.origin}${link.getAttribute('href')}`;
if (Logger.isLinkProcessed(rawTarget)) {
Logger.addLog(`⚠️ 该资源已在处理中,请勿重复点击`, 'process');
return;
}
Logger.markLinkAsProcessed(rawTarget);
Logger.startNewTask(rawTarget);
Logger.addLog(`开始处理链接 [${logType}] <a href="${rawTarget}" target="_blank">${rawTarget}</a>`,'process');
const finalType = (type === 'free' || type === 'unlocked') ? 'free' : type;
const processedTarget=`${rawTarget}?autotransfer=1&type=${finalType}`;
window.open(processedTarget,'_blank',`width=300,height=200,left=0,top=100,resizable=yes,scrollbars=yes`);
};
const descriptionElement = container.querySelector('p[class*="MuiTypography-root"][class*="MuiTypography-body2"][aria-label]');
if (descriptionElement) {
const descriptionContainer = descriptionElement.parentElement;
const buttonWrapper = document.createElement('div');
buttonWrapper.style.marginTop = '8px';
buttonWrapper.style.width = '100%';
buttonWrapper.style.display = 'flex';
buttonWrapper.style.justifyContent = 'flex-end';
buttonWrapper.appendChild(btn);
descriptionContainer.appendChild(buttonWrapper);
} else {
const topSection = container.querySelector('[class*="MuiStack-root"]');
if (topSection) {
const buttonWrapper = document.createElement('div');
buttonWrapper.style.marginTop = '8px';
buttonWrapper.style.width = '100%';
buttonWrapper.style.display = 'flex';
buttonWrapper.style.justifyContent = 'flex-end';
buttonWrapper.appendChild(btn);
topSection.parentNode.insertBefore(buttonWrapper, topSection.nextSibling);
}
}
});
}
function initParentPage() {
window.addEventListener('message',(event)=>{
if(event.data && event.data.type==='HDHIVE_RESULT'){
const {status,url,error,step}=event.data;
if(status==='success') {
if (Logger.isLinkProcessed(url)) {
return;
}
Logger.markLinkAsProcessed(url);
Logger.addLog(`✅ <b>获取成功</b>: <a href="${url}" target="_blank">${url}</a>`,'success');
handleTransfer115(url);
}
else if(status==='process') {
if (!Logger.isLinkProcessed(`process_${step}`)) {
Logger.markLinkAsProcessed(`process_${step}`);
Logger.addLog(`👉 ${step}`,'process');
}
}
else if(status==='error') Logger.addLog(`❌ <b>失败</b>: ${error}`,'error');
}
});
async function handleTransfer115(url) {
const enableTransfer = GM_getValue('115_enable_transfer', true);
if (!enableTransfer) {
Logger.addLog(`<span style="color:#ff9800;">⚠️ 未开启转存,取消转存</span>`, 'error');
Logger.endCurrentTask('completed');
return;
}
const transferMethod = GM_getValue('115_transfer_method', 'cookie');
const cid = GM_getValue('115_cid') || '0';
const transferKey = `transfer_${url}`;
if (Logger.isLinkProcessed(transferKey)) {
return;
}
Logger.markLinkAsProcessed(transferKey);
let result;
if (transferMethod === 'cookie') {
const cookie = GM_getValue('115_cookie');
if (!cookie) {
Logger.addLog('未填写115 Cookie,取消转存', 'process');
Logger.endCurrentTask('failed');
return;
}
result = await Transfer115.transfer(url, cookie, cid);
} else if (transferMethod === 'symedia') {
const symediaUrl = GM_getValue('symedia_url', '');
const symediaToken = GM_getValue('symedia_token', 'symedia');
if (!symediaUrl) {
Logger.addLog('未填写Symedia 地址,取消转存', 'process');
Logger.endCurrentTask('failed');
return;
}
result = await Transfer115.transferBySymedia(url, symediaUrl, symediaToken, cid);
} else {
Logger.addLog('未知的转存方式', 'error');
Logger.endCurrentTask('failed');
return;
}
if (result.success) {
Logger.addLog(result.message, 'success');
Logger.endCurrentTask('completed');
} else {
if (!result.message.includes('❌') && !result.message.includes('⚠️')) {
Logger.addLog(`❌ ${result.message}`, 'error');
} else {
Logger.addLog(result.message, 'error');
}
Logger.endCurrentTask('failed');
}
}
function is115Tab(){return document.querySelector('[class*="MuiTab-root"][class*="mui-"][class*="selected"], [class*="MuiTab-root"][class*="Mui-selected"]')?.textContent?.includes('115网盘');}
function startObserver(){
new MutationObserver(()=>{
if(is115Tab()) {
addButtons();
} else {
removeButtons();
}
}).observe(document.body,{childList:true,subtree:true});
if(is115Tab()) addButtons();
}
if (Utils.isUserPage()) {
new MutationObserver(async () => {
await processUserPageButtons();
}).observe(document.body, { childList: true, subtree: true });
processUserPageButtons();
} else {
addButtons();
startObserver();
}
}
function initChildPage(){
const params=new URLSearchParams(location.search);
if(!params.has('autotransfer')) return;
const type=params.get('type');
let isFinished=false;
const send=(data)=>window.opener && window.opener.postMessage({type:'HDHIVE_RESULT',...data},'*');
const log=(step)=>send({status:'process',step});
const fail=(msg)=>{if(isFinished) return; isFinished=true; clearAllFinders(); send({status:'error',error:msg});};
const success=(rawUrl)=>{
if(isFinished) return;
const check=Utils.verifyAndFormatUrl(rawUrl);
if(check.success){
isFinished=true;
clearAllFinders();
send({status:'success',url:check.url});
setTimeout(()=>window.close(),CONFIG.autoCloseDelay);
} else {
if(type==='paid' || type==='user_auto') console.log("捕获到无效链接,非最终链接,忽略:",rawUrl);
else fail(`链接校验不通过: ${check.msg} (URL: ${rawUrl})`);
}
};
const finders=[];
function clearAllFinders(){finders.forEach(id=>{try{clearInterval(id);}catch(e){}; try{clearTimeout(id);}catch(e){};}); finders.length=0;}
function checkUnlockAndCost() {
const buttons=Array.from(document.querySelectorAll('button'));
const unlockBtn=buttons.find(btn=>{
const text=Utils.normalizeText(btn.textContent);
return text.includes('确定解锁')||(text.includes('解锁')&&!text.includes('取消')&&!text.includes('close'));
});
if(unlockBtn && !unlockBtn.dataset.clicked){
unlockBtn.dataset.clicked="true";
const boxRoots=Array.from(document.querySelectorAll('[class*="MuiBox-root"][class*="mui-"]'));
let pointsDiv = boxRoots.find(el=>el.textContent && (el.textContent.includes('积分解锁')||el.textContent.includes('需要使用')||el.textContent.includes('已解锁人数')));
if (!pointsDiv) {
const allElements = Array.from(document.querySelectorAll('*'));
pointsDiv = allElements.find(el => {
const text = el.textContent || '';
return (text.includes('已解锁人数') && text.includes('积分')) ||
(text.includes('需要使用') && text.includes('积分解锁'));
});
}
if (pointsDiv) {
const text=pointsDiv.textContent||'';
const unlockedMatch=text.match(/已解锁人数\s*[::]?\s*(\d+)/)||text.match(/已解锁\s*(\d+)/)||text.match(/(\d+)/);
const unlockedCount=unlockedMatch?unlockedMatch[1]:'未知';
const pointsMatch=text.match(/需要使用\s*(\d+)\s*积分/)||text.match(/消耗[::]?\s*(\d+)\s*积分/)||text.match(/(\d+)\s*积分/);
const pointsCount=pointsMatch?pointsMatch[1]:'未知';
log(`已解锁 ${unlockedCount} 人`);
log(`需要 ${pointsCount} 积分`);
}
log('找到解锁按钮');
if (Utils.isSafari) {
unlockBtn.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true,view:window}));
} else {
try{unlockBtn.click();}
catch(e){unlockBtn.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true,view:window})); log(' click() 失败,改用dispatchEvent');}
}
return true;
}
return false;
}
if(type === 'paid' || type === 'user_auto'){
const unlockFinder=setInterval(()=>{
if(isFinished) return;
checkUnlockAndCost();
}, 300);
finders.push(unlockFinder);
}
document.addEventListener('DOMContentLoaded',()=>{
const to=setTimeout(()=>{if(!isFinished) fail('操作超时 (未获取到有效带密码链接)');}, CONFIG.maxWaitTime);
finders.push(to);
const observer=new MutationObserver((mutations,obs)=>{
if(isFinished){obs.disconnect();return;}
const links=document.querySelectorAll('a');
for(let a of links){
if(a.href && a.href.includes('115')){
const check=Utils.verifyAndFormatUrl(a.href);
if(check.success){success(a.href);obs.disconnect();return;}
}
}
});
observer.observe(document.body,{childList:true,subtree:true});
});
const oldXhr=XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open=function(){
this.addEventListener('load',function(){
const txt=this.responseText;
if(txt && (txt.includes('115cdn.com') || txt.includes('115.com/s/'))){
const match=txt.match(/https?:\/\/[^\s"']+/);
if(match){
const check=Utils.verifyAndFormatUrl(match[0]);
if(check.success){
log('已拦截XHR中的解锁链接');
success(match[0]);
}
}
}
});
return oldXhr.apply(this,arguments);
};
const oldOpen=window.open;
window.open=function(url){
if(url && (url.includes('115cdn.com') || url.includes('115.com/s/'))){
const check=Utils.verifyAndFormatUrl(url);
if(check.success){log('已拦截window.open跳转');success(url);return null;}
}
return oldOpen.apply(this,arguments);
};
}
function initFinal115Page(){
if(!window.opener) return;
const check=Utils.verifyAndFormatUrl(location.href);
if(check.success){
window.opener.postMessage({type:'HDHIVE_RESULT',status:'success',url:check.url},'*');
window.close();
}
else{
window.opener.postMessage({type:'HDHIVE_RESULT',status:'error',error:`跳转链接无效: ${check.msg}`},'*');
}
}
function init() {
Logger.init();
if (Utils.isResourcePage()) {
initChildPage();
} else if (Utils.isFinal115Page()) {
initFinal115Page();
} else if (Utils.isHDHiveSite()) {
SettingsManager.addSettingButton();
processAllPosters();
setupSearchListener();
setupUrlChangeListener();
setupSearchDialogListener();
let mutationTimeout;
const observer = new MutationObserver(mutations => {
clearTimeout(mutationTimeout);
mutationTimeout = setTimeout(() => {
let shouldProcess = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldProcess = true;
break;
}
}
if (shouldProcess) {
processAllPosters();
if (Utils.isUserPage()) {
setTimeout(() => FilterManager.apply(), 100);
}
}
}, 300);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
if (Utils.isParentPage() || Utils.isUserPage()) {
initParentPage();
}
}
}
function setupSearchListener() {
const searchInput = document.querySelector('input[type="text"][name="search"]');
if (searchInput) {
searchInput.addEventListener('input', () => {
state.processedItems.clear();
state.processingItems.clear();
setTimeout(processAllPosters, 1000);
});
}
}
function setupUrlChangeListener() {
let currentUrl = window.location.href;
const observer = new MutationObserver(() => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
state.processedItems.clear();
state.processingItems.clear();
state.embyCache.clear();
setTimeout(processAllPosters, 1000);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function setupSearchDialogListener() {
const observer = new MutationObserver(() => {
const searchDialog = document.querySelector('.MuiDialog-paper');
if (searchDialog) {
state.processedItems.clear();
state.processingItems.clear();
setTimeout(processAllPosters, 500);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
const style = document.createElement('style');
style.textContent = `
.emby-poster-btn {
position: absolute;
width: ${BUTTON_STYLES.posterBtn.size};
height: ${BUTTON_STYLES.posterBtn.size};
top: 10px;
left: 10px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
cursor: pointer;
z-index: 100;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.emby-poster-btn.has {
background: ${BUTTON_STYLES.posterBtn.has.bg};
color: white;
border: ${BUTTON_STYLES.posterBtn.has.border};
}
.emby-poster-btn.not-has {
background: ${BUTTON_STYLES.posterBtn.notHas.bg};
color: white;
border: ${BUTTON_STYLES.posterBtn.notHas.border};
}
.emby-poster-btn:hover {
transform: ${BUTTON_STYLES.posterBtn.hoverEffect};
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.emby-name-btn {
display: inline-flex;
align-items: center;
margin-top: ${BUTTON_STYLES.nameBtn.marginTop};
padding: ${BUTTON_STYLES.nameBtn.padding};
border-radius: 12px;
font-size: ${BUTTON_STYLES.nameBtn.fontSize};
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: transparent;
}
.emby-name-btn.has {
background: ${BUTTON_STYLES.nameBtn.has.bg};
color: ${BUTTON_STYLES.nameBtn.has.textColor};
border: ${BUTTON_STYLES.nameBtn.has.border};
}
.emby-name-btn.not-has {
background: ${BUTTON_STYLES.nameBtn.notHas.bg};
color: ${BUTTON_STYLES.nameBtn.notHas.textColor};
border: ${BUTTON_STYLES.nameBtn.notHas.border};
}
.emby-name-btn:hover {
transform: ${BUTTON_STYLES.nameBtn.hoverEffect};
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.emby-detail-poster-btn {
position: absolute;
width: ${BUTTON_STYLES.detailBtn.posterBtn.size};
height: ${BUTTON_STYLES.detailBtn.posterBtn.size};
top: 15px;
left: 15px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
cursor: pointer;
z-index: 100;
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.emby-detail-poster-btn.has {
background: ${BUTTON_STYLES.detailBtn.posterBtn.has.bg};
color: white;
border: ${BUTTON_STYLES.detailBtn.posterBtn.has.border};
}
.emby-detail-poster-btn.not-has {
background: ${BUTTON_STYLES.detailBtn.posterBtn.notHas.bg};
color: white;
border: ${BUTTON_STYLES.detailBtn.posterBtn.notHas.border};
}
.emby-detail-poster-btn:hover {
transform: ${BUTTON_STYLES.detailBtn.posterBtn.hoverEffect};
box-shadow: 0 8px 25px rgba(0,0,0,0.4);
}
.emby-detail-title-btn {
display: inline-flex;
align-items: center;
margin-left: ${BUTTON_STYLES.detailBtn.titleBtn.marginLeft};
padding: ${BUTTON_STYLES.detailBtn.titleBtn.padding};
border-radius: 15px;
font-size: ${BUTTON_STYLES.detailBtn.titleBtn.fontSize};
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: transparent;
}
.emby-detail-title-btn.has {
background: ${BUTTON_STYLES.detailBtn.titleBtn.has.bg};
color: ${BUTTON_STYLES.detailBtn.titleBtn.has.textColor};
border: ${BUTTON_STYLES.detailBtn.titleBtn.has.border};
}
.emby-detail-title-btn.not-has {
background: ${BUTTON_STYLES.detailBtn.titleBtn.notHas.bg};
color: ${BUTTON_STYLES.detailBtn.titleBtn.notHas.textColor};
border: ${BUTTON_STYLES.detailBtn.titleBtn.notHas.border};
}
.emby-detail-title-btn:hover {
transform: ${BUTTON_STYLES.detailBtn.titleBtn.hoverEffect};
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.emby-search-year-btn {
display: inline-flex;
align-items: center;
margin-left: ${BUTTON_STYLES.searchYearBtn.marginLeft};
padding: ${BUTTON_STYLES.searchYearBtn.padding};
border-radius: 12px;
font-size: ${BUTTON_STYLES.searchYearBtn.fontSize};
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: transparent;
}
.emby-search-year-btn.has {
background: ${BUTTON_STYLES.searchYearBtn.has.bg};
color: ${BUTTON_STYLES.searchYearBtn.has.textColor};
border: ${BUTTON_STYLES.searchYearBtn.has.border};
}
.emby-search-year-btn.not-has {
background: ${BUTTON_STYLES.searchYearBtn.notHas.bg};
color: ${BUTTON_STYLES.searchYearBtn.notHas.textColor};
border: ${BUTTON_STYLES.searchYearBtn.notHas.border};
}
.emby-search-year-btn:hover {
transform: ${BUTTON_STYLES.searchYearBtn.hoverEffect};
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.emby-user-page-btn {
display: inline-flex;
align-items: center;
margin-left: ${BUTTON_STYLES.userPageBtn.marginLeft};
padding: ${BUTTON_STYLES.userPageBtn.padding};
border-radius: 12px;
font-size: ${BUTTON_STYLES.userPageBtn.fontSize};
font-weight: 600;
cursor: default;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
opacity: 0.7;
}
.emby-user-page-btn.has {
background: ${BUTTON_STYLES.userPageBtn.has.bg};
color: ${BUTTON_STYLES.userPageBtn.has.textColor};
border: ${BUTTON_STYLES.userPageBtn.has.border};
}
.emby-user-page-btn.not-has {
background: ${BUTTON_STYLES.userPageBtn.notHas.bg};
color: ${BUTTON_STYLES.userPageBtn.notHas.textColor};
border: ${BUTTON_STYLES.userPageBtn.notHas.border};
}
.emby-user-page-btn:hover {
transform: ${BUTTON_STYLES.userPageBtn.hoverEffect};
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.emby-collection-btn {
display: inline-flex;
align-items: center;
margin-left: ${BUTTON_STYLES.collectionBtn.marginLeft};
padding: ${BUTTON_STYLES.collectionBtn.padding};
border-radius: 12px;
font-size: ${BUTTON_STYLES.collectionBtn.fontSize};
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: transparent;
}
.emby-collection-btn.has {
background: ${BUTTON_STYLES.collectionBtn.has.bg};
color: ${BUTTON_STYLES.collectionBtn.has.textColor};
border: ${BUTTON_STYLES.collectionBtn.has.border};
}
.emby-collection-btn.not-has {
background: ${BUTTON_STYLES.collectionBtn.notHas.bg};
color: ${BUTTON_STYLES.collectionBtn.notHas.textColor};
border: ${BUTTON_STYLES.collectionBtn.notHas.border};
}
.emby-collection-btn:hover {
transform: ${BUTTON_STYLES.collectionBtn.hoverEffect};
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.emby-setting-btn {
display: inline-flex;
align-items: center;
padding: ${BUTTON_STYLES.settingBtn.padding};
margin-left: 10px;
border-radius: 14px;
font-size: ${BUTTON_STYLES.settingBtn.fontSize};
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: ${BUTTON_STYLES.settingBtn.has.bg};
color: ${BUTTON_STYLES.settingBtn.has.textColor};
border: ${BUTTON_STYLES.settingBtn.has.border};
vertical-align: middle;
}
.emby-setting-btn:hover {
transform: ${BUTTON_STYLES.settingBtn.hoverEffect};
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.emby-name-btn::before,
.emby-detail-title-btn::before,
.emby-search-year-btn::before,
.emby-user-page-btn::before,
.emby-collection-btn::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
margin-right: 6px;
background-image: url('https://raw.githubusercontent.com/lige47/QuanX-icon-rule/main/icon/04ProxySoft/emby.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
filter: brightness(0.9);
}
.emby-detail-title-btn::before {
width: 18px;
height: 18px;
margin-right: 8px;
}
.MuiPopover-root .emby-poster-btn,
.MuiPopover-root .emby-name-btn,
.MuiPopover-root .emby-detail-poster-btn,
.MuiPopover-root .emby-detail-title-btn,
.MuiPopover-root .emby-search-year-btn,
.MuiPopover-root .emby-user-page-btn,
.MuiPopover-root .emby-collection-btn,
.MuiPopover-root .emby-setting-btn {
z-index: 1500;
}
#hdhive-notice {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
padding: 8px 12px;
background-color: #ff9800;
color: #fff;
font-size: 14px;
font-weight: bold;
border-radius: 4px;
z-index: 9999;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
`;
document.head.appendChild(style);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();