// ==UserScript==
// @name 豆瓣+TMDB影视工具(图片去重与全展示优化版)
// @namespace tampermonkey
// @version 24.85
// @description 影视信息提取与排版工具,优化图片重复与剧照展示(海报滚轮分页加载优化)
// @author 豆包
// @match https://pan1.me/?thread-create-*.htm
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_log
// @run-at document-end
// @license MIT // 新增:声明MIT许可证
// ==/UserScript==
(function () {
'use strict';
// 核心配置
const TMDB_CONFIG = {
API_KEY: '860ffcf671f5c664bbadeb5a972f9650',
ACCESS_TOKEN: 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI4NjBmZmNmNjcxZjVjNjY0YmJhZGViNWE5NzJmOTY1MCIsIm5iZiI6MTc1NjE3NDMxMS40NDIsInN1YiI6IjY4YWQxN2U3YjYzZDI2MWNlNDQ0OTZlMiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.XXHfRXpDp7S42rK5PdptQ6xGwtG5ohnXYTfdILwVxXg',
BASE_URL: 'https://api.themoviedb.org/3',
IMAGE_BASE_URL: 'https://image.tmdb.org/t/p/',
POSTER_SIZE: 'w1280',
STILL_SIZE: 'w1280',
DOUBAN_QUALITY: {
PRIORITY: ['raw', 'l', 'm'],
TIMEOUT: 3000,
RETRY: 1
},
IMAGE_CANDIDATES_COUNT: 5, // 每组加载数量(海报、剧照通用)
POSTER_PER_ROW: 5,
STILL_PER_ROW: 5
};
const COMMON_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
};
// 存储变量
let selectedPosterUrl = '';
let selectedStillUrl = '';
let currentMovieInfo = null;
let currentComments = [];
let sourceCodeElement = null;
let panelObserver = null;
let isPanelInitialized = false;
let currentEditor = null;
let posterPage = 0;
let isLoadingPosters = false;
let isLoadingStills = false;
let posterContainer = null;
let stillContainer = null;
let panel = null;
let loadedPosterIds = new Set(); // 海报去重:记录已加载的海报唯一ID
let stillPage = 0; // 剧照分页标记
let loadedStillIds = new Set(); // 剧照去重:记录已加载的剧照唯一ID
// 排版美化样式库
const FORMAT_STYLES = [
{
name: '主标题',
icon: 'fa-header',
tag: 'h1',
category: '标题',
styles: {
'color': '#1e40af',
'font-size': '24px',
'font-weight': 'bold',
'margin': '20px 0 15px 0',
'padding-bottom': '8px',
'border-bottom': '2px solid #dbeafe'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '主标题示例';
return `<h1 style="color:#1e40af;font-size:24px;font-weight:bold;margin:20px 0 15px 0;padding-bottom:8px;border-bottom:2px solid #dbeafe;">${content}</h1>`;
}
},
{
name: '副标题',
icon: 'fa-header',
tag: 'h2',
category: '标题',
styles: {
'color': '#2563eb',
'font-size': '20px',
'font-weight': 'bold',
'margin': '18px 0 12px 0',
'padding-bottom': '5px',
'border-bottom': '1px solid #dbeafe'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '副标题示例';
return `<h2 style="color:#2563eb;font-size:20px;font-weight:bold;margin:18px 0 12px 0;padding-bottom:5px;border-bottom:1px solid #dbeafe;">${content}</h2>`;
}
},
{
name: '三级标题',
icon: 'fa-header',
tag: 'h3',
category: '标题',
styles: {
'color': '#3b82f6',
'font-size': '18px',
'font-weight': 'bold',
'margin': '15px 0 10px 0'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '三级标题示例';
return `<h3 style="color:#3b82f6;font-size:18px;font-weight:bold;margin:15px 0 10px 0;">${content}</h3>`;
}
},
{
name: '正文段落',
icon: 'fa-paragraph',
tag: 'p',
category: '文本',
styles: {
'color': '#333',
'font-size': '14px',
'line-height': '1.8',
'margin': '8px 0',
'text-indent': '2em'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '这是一段正文示例,包含标准的段落格式和首行缩进,适合用于大部分内容的展示。';
return `<p style="color:#333;font-size:14px;line-height:1.8;margin:8px 0;text-indent:2em;">${content}</p>`;
}
},
{
name: '引用文本',
icon: 'fa-quote-right',
tag: 'blockquote',
category: '文本',
styles: {
'color': '#666',
'font-size': '13px',
'line-height': '1.6',
'margin': '10px 0',
'padding': '10px 15px',
'border-left': '3px solid #2196F3',
'background': '#f8f9fa'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '这是一段引用文本示例,通常用于引用他人的话语或特殊说明内容。';
return `<blockquote style="color:#666;font-size:13px;line-height:1.6;margin:10px 0;padding:10px 15px;border-left:3px solid #2196F3;background:#f8f9fa;">${content}</blockquote>`;
}
},
{
name: '无序列表',
icon: 'fa-list-ul',
tag: 'ul',
category: '列表',
styles: {
'margin': '10px 0 10px 20px',
'padding': '0'
},
itemStyles: {
'color': '#444',
'font-size': '14px',
'line-height': '1.7',
'margin': '5px 0',
'list-style-type': 'disc'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '列表项1\n列表项2\n列表项3';
const items = content.split('\n').map(item =>
`<li style="color:#444;font-size:14px;line-height:1.7;margin:5px 0;list-style-type:disc;">${item.trim()}</li>`
).join('');
return `<ul style="margin:10px 0 10px 20px;padding:0;">${items}</ul>`;
}
},
{
name: '有序列表',
icon: 'fa-list-ol',
tag: 'ol',
category: '列表',
styles: {
'margin': '10px 0 10px 20px',
'padding': '0'
},
itemStyles: {
'color': '#444',
'font-size': '14px',
'line-height': '1.7',
'margin': '5px 0'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '步骤一\n步骤二\n步骤三';
const items = content.split('\n').map(item =>
`<li style="color:#444;font-size:14px;line-height:1.7;margin:5px 0;">${item.trim()}</li>`
).join('');
return `<ol style="margin:10px 0 10px 20px;padding:0;">${items}</ol>`;
}
},
{
name: '分隔线',
icon: 'fa-minus',
tag: 'hr',
category: '布局',
styles: {
'border': 'none',
'border-top': '1px solid #e0e0e0',
'margin': '20px 0',
'height': '1px'
},
preview: true,
apply: () => {
return `<hr style="border:none;border-top:1px solid #e0e0e0;margin:20px 0;height:1px;">`;
}
},
{
name: '高亮文本',
icon: 'fa-highlighter',
tag: 'span',
category: '文本',
styles: {
'background': '#fff380',
'padding': '0 3px',
'border-radius': '2px'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '需要高亮的文本';
return `<span style="background:#fff380;padding:0 3px;border-radius:2px;">${content}</span>`;
}
},
{
name: '链接样式',
icon: 'fa-link',
tag: 'a',
category: '文本',
styles: {
'color': '#2563eb',
'text-decoration': 'none',
'border-bottom': '1px dashed #93c5fd',
'padding': '0 1px'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '链接文本';
return `<a href="#" style="color:#2563eb;text-decoration:none;border-bottom:1px dashed #93c5fd;padding:0 1px;">${content}</a>`;
}
},
{
name: '居中文本',
icon: 'fa-align-center',
tag: 'div',
category: '布局',
styles: {
'text-align': 'center',
'margin': '10px 0',
'color': '#4b5563'
},
preview: true,
apply: (selectedText) => {
const content = selectedText || '这段文本会居中显示';
return `<div style="text-align:center;margin:10px 0;color:#4b5563;">${content}</div>`;
}
},
{
name: '影视卡片',
icon: 'fa-film',
tag: 'div',
category: '特殊',
preview: true,
apply: (selectedText) => {
const title = selectedText || '影视名称';
return `
<div style="border:1px solid #e5e7eb;border-radius:6px;padding:15px;margin:15px 0;box-shadow:0 1px 3px rgba(0,0,0,0.05);">
<h3 style="margin-top:0;color:#1e40af;">${title}</h3>
<p style="margin-bottom:0;color:#4b5563;font-size:14px;">这里可以添加影视的简要说明或推荐理由...</p>
</div>
`;
}
}
];
// 去重工具函数:过滤重复的图片URL
function removeDuplicateUrls(urls) {
return [...new Set(urls)].filter(url => url.trim() !== '');
}
// 自动填充和保存相关函数
function autoClickSourceBtn() {
return new Promise((resolve) => {
const modalSourceBtn = document.querySelector('#myModal-code .btn, #source-code-btn');
if (modalSourceBtn && modalSourceBtn.textContent.includes('源代码')) {
modalSourceBtn.click();
setTimeout(() => resolve(true), 600);
return;
}
const textButtons = [...document.querySelectorAll('button'), ...document.querySelectorAll('a')]
.filter(elem => elem.textContent.trim().includes('源代码'));
if (textButtons.length > 0) {
textButtons[0].click();
setTimeout(() => resolve(true), 300);
return;
}
const tinyMceBtn = document.querySelector('.tox-tbtn[title="源代码"]');
const oldTinyMceBtn = Array.from(document.querySelectorAll('.mce-btn')).find(elem => elem.textContent.includes('源代码'));
const ckSourceLabel = document.querySelector('.cke_button__source_label');
if (tinyMceBtn) {
tinyMceBtn.click();
setTimeout(() => resolve(true), 300);
} else if (oldTinyMceBtn) {
oldTinyMceBtn.click();
setTimeout(() => resolve(true), 300);
} else if (ckSourceLabel && ckSourceLabel.closest('.cke_button')) {
ckSourceLabel.closest('.cke_button').click();
setTimeout(() => resolve(true), 300);
} else {
resolve(true);
}
});
}
function autoFillSourceBox(html) {
return new Promise((resolve) => {
let retryCount = 0;
const maxRetry = 20;
const interval = 300;
const tryFill = setInterval(() => {
retryCount++;
const editorSelectors = [
'#myModal-code textarea',
'textarea.tox-textarea',
'textarea.mce-textbox',
'textarea.cke_source',
'textarea[name="message"]',
'textarea[name="content"]',
'#editor_content',
'#content',
'textarea[rows="20"][cols="80"]',
'.CodeMirror textarea',
'.editor-textarea'
];
let targetBox = null;
for (const selector of editorSelectors) {
const elem = document.querySelector(selector);
if (elem && elem.style.display !== 'none' && elem.offsetParent !== null) {
targetBox = elem;
sourceCodeElement = elem;
currentEditor = getCurrentEditor();
break;
}
}
if (targetBox) {
const codeMirror = targetBox.closest('.CodeMirror');
if (codeMirror && codeMirror.CodeMirror) {
codeMirror.CodeMirror.setValue(html);
codeMirror.CodeMirror.getDoc().markClean();
codeMirror.CodeMirror.getDoc().changed();
} else {
targetBox.value = html;
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
const blurEvent = new Event('blur', { bubbles: true, cancelable: true });
targetBox.dispatchEvent(inputEvent);
targetBox.dispatchEvent(changeEvent);
targetBox.dispatchEvent(blurEvent);
targetBox.focus();
targetBox.setSelectionRange(html.length, html.length);
}
clearInterval(tryFill);
resolve(true);
return;
}
if (retryCount >= maxRetry) {
clearInterval(tryFill);
resolve(false);
}
}, interval);
});
}
function autoClickSaveBtn() {
return new Promise((resolve) => {
const saveButtons = [
...document.querySelectorAll('button'),
...document.querySelectorAll('a')
].filter(elem => {
const text = elem.textContent.trim();
return text === '保存' || text === '保存草稿' || text === '保存内容';
});
if (saveButtons.length > 0) {
saveButtons[0].click();
setTimeout(() => resolve(true), 500);
return;
}
const commonSaveBtn = document.querySelector('button.save, .save-button, [type="submit"][value="保存"]');
if (commonSaveBtn && !commonSaveBtn.textContent.includes('发布')) {
commonSaveBtn.click();
setTimeout(() => resolve(true), 500);
return;
}
resolve(false);
});
}
function autoFillTitleInput(title) {
return new Promise((resolve) => {
const titleInput = document.querySelector('input[placeholder="标题"], input[name="title"], #title');
if (!titleInput) {
resolve(false);
return;
}
titleInput.value = title || (currentMovieInfo?.title || '影视内容分享');
titleInput.dispatchEvent(new Event('input', { bubbles: true }));
titleInput.dispatchEvent(new Event('change', { bubbles: true }));
// 模拟键盘事件确保内容被检测
const keydownEvent = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true });
const keyupEvent = new KeyboardEvent('keyup', { key: 'Enter', bubbles: true });
titleInput.dispatchEvent(keydownEvent);
titleInput.dispatchEvent(keyupEvent);
resolve(true);
});
}
async function fillAndSaveSource(html) {
// 强制填充标题,解决标题缺失提示
await autoFillTitleInput(currentMovieInfo?.title);
showStatus('正在切换到源代码模式...', false);
const switched = await autoClickSourceBtn();
if (!switched) {
showStatus('未检测到源代码按钮,尝试直接填充...', false);
}
showStatus('正在填充内容到编辑框...', false);
const filled = await autoFillSourceBox(html);
if (filled) {
showStatus('内容填充完成,正在自动保存编辑内容(非发布)...', false);
const saved = await autoClickSaveBtn();
if (saved) {
showStatus('内容已填充并自动保存(非发布)', false);
} else {
showStatus('内容已填充,未找到“保存”按钮,请手动点击保存', false);
}
return true;
} else {
GM_setClipboard(html);
showStatus('自动填充失败,内容已复制到剪贴板,请手动粘贴', true);
const pasteBtn = document.getElementById('paste-btn');
if (pasteBtn) pasteBtn.style.display = 'inline-block';
return false;
}
}
// 内容生成函数
function generateHTML(movie, comments, posterDataURL, stillDataURL) {
const finalStillUrl = stillDataURL || 'https://picsum.photos/800/450?default-still';
const runtime = movie.runtime === 'null' || !movie.runtime ? '未知片长' : movie.runtime;
let imdbHtml = '';
if (movie.imdbId && movie.imdbId !== '暂无') {
imdbHtml = `<span> </span><strong style="box-sizing: border-box; font-weight: bolder;">IMDb:</strong><span style="box-sizing: border-box; color: rgb(0, 2, 255);"><a style="box-sizing: border-box; color: rgb(0, 2, 255); text-decoration: none; background-color: transparent; transition: 0.2s;" href="https://www.imdb.com/title/${movie.imdbId}/" target="_blank" rel="noopener"><span> </span>${movie.imdbId}</a></span>`;
}
const introHtml = movie.intro
.split('\n')
.filter(para => para.trim())
.map(para => `<div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">${para.trim()}</div>`)
.join('');
let commentsHtml = '';
if (comments && comments.length > 0) {
commentsHtml = `
<h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">影视热评:</h3>
<div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">${comments[0]}</div>
`;
}
return `
<div class="card border" style="box-sizing: border-box; position: relative; display: flex; flex-direction: column; min-width: 0px; overflow-wrap: break-word; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255); border: none; border-radius: 0.75rem; margin-bottom: 1rem; box-shadow: rgba(46, 45, 116, 0.05) 0px 0.25rem 1.875rem; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<div class="movie-info" style="box-sizing: border-box; flex: 1 1 400px; padding: 20px;"><img class="lazy img-responsive" style="box-sizing: border-box; vertical-align: middle; border: 1px solid transparent; max-width: 100%; -webkit-user-drag: none; margin-bottom: 0.5rem; height: auto; width: 816.818px; cursor: pointer;" src="${posterDataURL}" width="1080" height="1522" data-original="${posterDataURL}"><br style="box-sizing: border-box;">
<div class="movie-info-content" style="box-sizing: border-box;">
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">名称:</strong>${movie.title}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">又名:</strong>${movie.alsoKnown || '无'}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">导演:</strong>${movie.director}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">编剧:</strong>${movie.writer}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">主演:</strong>${movie.actor}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">类型:</strong>${movie.genreTags.join('、') || '未知'}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">制片地区:</strong>${movie.region}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">上映时间:</strong>${movie.release}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">影视语言:</strong>${movie.lang}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">评分:</strong><span style="box-sizing: border-box; color: rgb(0, 132, 255); font-weight: 600;"><span> </span>${movie.rating}</span><span> </span><strong style="box-sizing: border-box; font-weight: bolder;">豆瓣ID:</strong><span style="box-sizing: border-box; color: rgb(0, 2, 255);"><a style="box-sizing: border-box; color: rgb(0, 2, 255); text-decoration: none; background-color: transparent; transition: 0.2s;" href="${movie.mediaType === 'tv' ? 'https://tv.douban.com/subject/' : 'https://movie.douban.com/subject/'}${movie.doubanId || movie.tmdbId}/" target="_blank" rel="noopener"><span> </span>${movie.doubanId || movie.tmdbId}</a></span>${imdbHtml}</p>
<p style="box-sizing: border-box; margin: 0.2rem 0px; line-height: 1.7;"><strong style="box-sizing: border-box; font-weight: bolder;">片长:</strong>${runtime}</p>
</div>
</div>
</div>
<h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">影视简介:</h3>
${introHtml}
<div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"> </div>
<div style="box-sizing: border-box; color: rgb(33, 37, 41); font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<h3 style="box-sizing: border-box; margin-top: 0px; margin-bottom: 0.5rem; font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 微软雅黑, 华文细黑, STHeiti, sans-serif; font-weight: 500; line-height: 1.2; color: rgb(33, 37, 41); font-size: 1.75rem; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">精彩剧照:</h3>
<img src="${finalStillUrl}" style="box-sizing: border-box; vertical-align: middle; border-style: none; max-width: 100%; height: auto; border-radius: 4px; margin-bottom: 1rem;" alt="${movie.title} 剧照">
</div>
${commentsHtml}
`;
}
// 控制面板创建与定位 - 精准放置到标记位置
function createPanel() {
panel = document.createElement('div');
panel.id = 'douban-tmdb-panel';
panel.style.cssText = `
background: #fff; border: 1px solid #e5e7eb; border-radius: 6px;
padding: 10px; margin: 8px 0; box-shadow: 0 1px 2px rgba(0,0,0,0.05);
z-index: 999; position: relative;
box-sizing: border-box;
width: 100%; max-width: 1000px;
`;
panel.innerHTML = `
<div style="margin:0 0 10px 0; font-size:16px; font-weight:600; color:#374151; border-bottom:1px solid #e5e7eb; padding-bottom:6px;">豆瓣+TMDB影视工具</div>
<!-- 排版美化工具区域 -->
<div style="margin-bottom:12px; padding:8px; background:#f9fafb; border-radius:4px;">
<h4 style="margin:0 0 8px 0; color:#4b5563; font-size:14px; display:flex; justify-content:space-between; align-items:center;">
排版美化工具
<span id="format-preview-toggle" style="font-size:12px; color:#3b82f6; cursor:pointer; text-decoration:underline;">显示预览</span>
</h4>
<!-- 样式分类标签 -->
<div id="format-categories" style="display:flex; gap:8px; margin-bottom:8px; overflow-x:auto; padding-bottom:4px; border-bottom:1px solid #e2e8f0;">
<!-- 分类标签通过JS生成 -->
</div>
<!-- 样式按钮区域 -->
<div style="display:flex; flex-wrap:wrap; gap:6px;" id="format-buttons">
<!-- 美化按钮通过JS生成 -->
</div>
<!-- 样式预览区域 -->
<div id="format-preview" style="margin-top:10px; padding:10px; background:white; border:1px solid #e5e7eb; border-radius:4px; display:none; max-height:200px; overflow-y:auto; font-family:'Microsoft YaHei', sans-serif;">
<div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div>
</div>
</div>
<div style="margin-bottom:10px; position:relative;">
<label style="display:inline-block; width:70px; font-weight:500; color:#4b5563;">搜索影片:</label>
<input type="text" id="search-movie" placeholder="直接输入名称(如:奥本海默)" style="width: calc(100% - 80px); padding:5px; border:1px solid #d1d5db; border-radius:4px; font-size:13px;">
<div id="search-loading" style="position:absolute; right:10px; top:6px; color:#9ca3af; display:none;">
<i>搜索中...</i>
</div>
<div id="search-results" style="position:absolute; z-index:1000; background:#fff; border:1px solid #d1d5db; border-radius:4px; max-height:300px; overflow-y:auto; width: calc(100% - 80px); left:70px; top: 32px; display:none;"></div>
</div>
<div style="margin-bottom:8px;">
<label style="display:inline-block; width:70px; font-weight:500; color:#4b5563;">影视链接:</label>
<input type="text" id="media-url" placeholder="豆瓣或TMDB链接" style="width: calc(100% - 80px); padding:5px; border:1px solid #d1d5db; border-radius:4px; font-size:13px;">
</div>
<div style="margin-bottom:8px; display:flex; align-items:center;">
<button id="fetch-btn" style="background:#3b82f6; color:white; border:none; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; display:none;">提取并填充</button>
<button id="paste-btn" style="background:#8b5cf6; color:white; border:none; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; display:none; margin-left:8px;">手动复制内容</button>
<textarea id="backup-html" style="display:none;"></textarea>
</div>
<!-- 选择海报区域(Grid 布局 + 滚动条) -->
<div id="image-selection" style="margin-top:12px; display:none;">
<h4 style="color:#3b82f6; margin-bottom:8px; font-size:14px;">选择海报(点击图片选择):</h4>
<div id="poster-candidates" style="display: grid; grid-template-columns: repeat(5, 1fr); gap:8px; padding:8px; margin-bottom:10px; border:1px solid #e5e7eb; border-radius:4px; min-height:200px; max-height:400px; overflow-y:auto;"></div>
<button id="load-more-posters" style="display: none; background:#60a5fa; color:white; border:none; padding:4px 10px; border-radius:3px; cursor:pointer; font-size:12px; margin-bottom:15px;">加载更多海报</button>
<h4 style="color:#3b82f6; margin-bottom:8px; font-size:14px;">选择剧照(点击图片选择):</h4>
<div id="still-candidates" style="display: none; grid-template-columns: repeat(5, 1fr); gap:8px; padding:8px; margin-bottom:10px; border:1px solid #e5e7eb; border-radius:4px; min-height:200px; max-height:400px; overflow-y:auto;"></div>
<button id="load-more-stills" style="display: none; background:#60a5fa; color:white; border:none; padding:4px 10px; border-radius:3px; cursor:pointer; font-size:12px; margin-bottom:15px;">加载更多剧照</button>
<!-- 清除按钮与确认按钮同组 -->
<div style="display:flex; align-items:center; margin-top:10px;">
<button id="clear-btn" style="background:#ef4444; color:white; border:none; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; margin-right:8px;">清除</button>
<button id="confirm-images-btn" style="background:#10b981; color:white; border:none; padding:6px 16px; border-radius:4px; cursor:pointer; font-size:13px;">确认选择并填充(自动保存编辑内容)</button>
</div>
</div>
<div id="status" style="margin-top:8px; padding:6px; border-radius:4px; font-size:12px; display:none;"></div>
`;
return panel;
}
// 插入面板到图片标记的固定位置(精准定位)
function insertPanelInMarkedPosition() {
if (isPanelInitialized) return;
// 创建面板
panel = createPanel();
// 扩展目标容器选择器 + 空值安全处理
const targetContainers = [
// 优先匹配内容区域主容器
document.querySelector('.main-content'),
// 对可能为 null 的选择器进行空值包装
(document.querySelector('#thread-create-form') || {}).parentElement,
(document.querySelector('.post-form') || {}).parentElement,
document.querySelector('.panel-default'),
document.body // 最终 fallback 到 body
];
let targetContainer = null;
// 遍历找到第一个有效容器
for (const container of targetContainers) {
if (container && container.offsetParent !== null) {
targetContainer = container;
break;
}
}
// 确保总有有效容器(兜底)
if (!targetContainer) {
targetContainer = document.body;
console.warn('无法找到目标容器,已 fallback 到 document.body');
}
// 移除旧面板(若存在)
const oldPanel = targetContainer.querySelector('#douban-tmdb-panel');
if (oldPanel) targetContainer.removeChild(oldPanel);
// 插入新面板到容器顶部
if (targetContainer.firstChild) {
targetContainer.insertBefore(panel, targetContainer.firstChild);
} else {
targetContainer.appendChild(panel);
}
// 初始化组件
posterContainer = document.getElementById('poster-candidates');
stillContainer = document.getElementById('still-candidates');
initFormatTools();
bindEventListeners();
setupMutationObserver(targetContainer);
isPanelInitialized = true;
showStatus('控制面板已放置在可用位置', false);
return true;
}
insertPanelInMarkedPosition();
// 监听父容器变化,确保面板位置不移动
function setupMutationObserver(parent) {
if (panelObserver) {
panelObserver.disconnect();
}
panelObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
const panel = document.getElementById('douban-tmdb-panel');
// 确保面板始终在父容器内,不被移动
if (!panel || !parent.contains(panel)) {
if (parent.firstChild) {
parent.insertBefore(panel || createPanel(), parent.firstChild);
} else {
parent.appendChild(panel || createPanel());
}
// 重新初始化以保持功能完整
posterContainer = document.getElementById('poster-candidates');
stillContainer = document.getElementById('still-candidates');
initFormatTools();
bindEventListeners();
}
});
});
panelObserver.observe(parent, {
childList: true,
attributes: true,
subtree: true,
characterData: true
});
}
// 工具函数
function showStatus(text, isError = false) {
const status = document.getElementById('status');
if (!status) return;
status.textContent = text;
status.style.display = 'block';
status.style.cssText = `
margin-top:8px; padding:6px; border-radius:4px; font-size:12px;
${isError ? 'background:#fee2e2; color:#b91c1c; border:1px solid #fecaca;' : 'background:#ecfdf5; color:#065f46; border:1px solid #d1fae5;'}
`;
}
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
function safeGet(obj, path, defaultValue = '') {
try {
return path.split('.').reduce((o, k) => o[k], obj) || defaultValue;
} catch (e) {
return defaultValue;
}
}
// 图片获取函数
function getImageDataURLWithQuality(url) {
return new Promise((resolve) => {
if (!url) {
resolve('https://picsum.photos/800/450?default-still');
return;
}
let baseUrl = url;
if (baseUrl.includes('doubanio.com') && !baseUrl.includes('https:')) {
baseUrl = 'https:' + baseUrl;
}
if (baseUrl.includes('doubanio.com') && baseUrl.includes('/m/')) {
const qualityUrls = TMDB_CONFIG.DOUBAN_QUALITY.PRIORITY.map(quality =>
baseUrl.replace('/m/', `/${quality}/`)
);
const tryQuality = (index, retryCount = 0) => {
if (index >= qualityUrls.length) {
getFallbackImageDataURL(baseUrl).then(resolve);
return;
}
const currentUrl = qualityUrls[index];
GM_xmlhttpRequest({
method: 'GET',
url: currentUrl,
headers: { ...COMMON_HEADERS, 'Referer': 'https://movie.douban.com/' },
responseType: 'blob',
timeout: TMDB_CONFIG.DOUBAN_QUALITY.TIMEOUT,
onload: (res) => {
if (res.status === 200 && res.response) {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.readAsDataURL(res.response);
} else if (retryCount < TMDB_CONFIG.DOUBAN_QUALITY.RETRY) {
setTimeout(() => tryQuality(index, retryCount + 1), 500);
} else {
tryQuality(index + 1);
}
},
onerror: () => {
if (retryCount < TMDB_CONFIG.DOUBAN_QUALITY.RETRY) {
setTimeout(() => tryQuality(index, retryCount + 1), 500);
} else {
tryQuality(index + 1);
}
},
ontimeout: () => {
if (retryCount < TMDB_CONFIG.DOUBAN_QUALITY.RETRY) {
setTimeout(() => tryQuality(index, retryCount + 1), 500);
} else {
tryQuality(index + 1);
}
}
});
};
tryQuality(0);
return;
}
getFallbackImageDataURL(baseUrl).then(resolve);
});
}
function getFallbackImageDataURL(url) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
...COMMON_HEADERS,
'Referer': url.includes('doubanio.com') ? 'https://movie.douban.com/' :
url.includes('themoviedb.org') ? 'https://www.themoviedb.org/' : ''
},
responseType: 'blob',
timeout: 5000,
onload: (res) => {
if (res.status === 200 && res.response) {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.readAsDataURL(res.response);
} else {
resolve('https://picsum.photos/800/450?error-still');
}
},
onerror: () => resolve('https://picsum.photos/800/450?error-still'),
ontimeout: () => resolve('https://picsum.photos/800/450?error-still')
});
});
}
// 确保URL有效性,保障加载更多功能(去重处理)
async function getDoubanOfficialPosters(subjectUrl) {
return new Promise(resolve => {
try {
const urlObj = new URL(subjectUrl);
// 访问豆瓣“全部图片”页面(type=R),获取更多海报
const photosUrl = subjectUrl.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=R');
GM_xmlhttpRequest({
method: 'GET',
url: photosUrl,
headers: { ...COMMON_HEADERS, 'Referer': subjectUrl, 'Host': urlObj.hostname },
timeout: 8000,
onload: (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
// 提取所有海报图片(含多页),并替换为高质量版本(m→l)
const posterImgs = Array.from(doc.querySelectorAll('.poster-col3 li img'))
.map(img => {
let src = img.src;
if (src.includes('/m/')) src = src.replace('/m/', '/l/'); // 低质量→高质量
return { url: src, id: src }; // 返回带id的对象(用url作为唯一标识)
})
.filter(Boolean);
// 去重处理(通过id)
const uniquePosters = [];
const idSet = new Set();
posterImgs.forEach(img => {
if (!idSet.has(img.id)) {
idSet.add(img.id);
uniquePosters.push(img);
}
});
resolve(uniquePosters.map(img => img.url)); // 仍返回url数组,保持原有逻辑兼容
} catch (e) {
console.error('解析豆瓣海报页面失败:', e);
resolve([]);
}
},
onerror: () => {
console.error('获取豆瓣海报请求错误');
resolve([]);
},
ontimeout: () => {
console.error('获取豆瓣海报请求超时');
resolve([]);
}
});
} catch (e) {
console.error('无效的豆瓣主题URL:', subjectUrl, e);
resolve([]);
}
});
}
// 确保URL有效性,保障剧照展示(去重处理)
function getDoubanStillsList(url) {
return new Promise(resolve => {
try {
const urlObj = new URL(url);
const stillsUrl = url.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/photos?type=still');
GM_xmlhttpRequest({
method: 'GET',
url: stillsUrl,
headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': urlObj.hostname },
timeout: 8000,
onload: (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const labeledStills = Array.from(doc.querySelectorAll('.poster-col3 li img[data-title*="剧照"]')).map(img => img.src).filter(Boolean);
const finalStills = labeledStills.length ? labeledStills : Array.from(doc.querySelectorAll('.poster-col3 li img')).map(img => img.src).filter(Boolean);
// 去重处理
const uniqueStills = removeDuplicateUrls(finalStills);
resolve(uniqueStills);
} catch (e) {
console.error('解析豆瓣剧照页面失败:', e);
resolve([]);
}
},
onerror: () => {
console.error('获取豆瓣剧照请求错误');
resolve([]);
},
ontimeout: () => {
console.error('获取豆瓣剧照请求超时');
resolve([]);
}
});
} catch (e) {
console.error('无效的豆瓣URL:', url, e);
resolve([]);
}
});
}
function getTMDBStillsList(mediaType, id) {
return new Promise(resolve => {
const stillCutsUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=still_cuts&sort_by=primary`;
GM_xmlhttpRequest({
method: 'GET',
url: stillCutsUrl,
headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
timeout: 10000,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const stillCuts = safeGet(data, 'still_cuts', []);
let stillUrls = stillCuts.map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.STILL_SIZE}/${img.file_path}`).filter(Boolean);
if (stillUrls.length >= TMDB_CONFIG.IMAGE_CANDIDATES_COUNT) {
// 去重处理
stillUrls = removeDuplicateUrls(stillUrls);
resolve(stillUrls);
return;
}
const backdropsUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=backdrop&sort_by=vote_average.desc`;
GM_xmlhttpRequest({
method: 'GET',
url: backdropsUrl,
headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const backdrops = safeGet(data, 'backdrops', []);
const backdropUrls = backdrops.map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.STILL_SIZE}/${img.file_path}`).filter(Boolean);
stillUrls = [...stillUrls, ...backdropUrls].slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT);
// 去重处理
stillUrls = removeDuplicateUrls(stillUrls);
if (stillUrls.length >= TMDB_CONFIG.IMAGE_CANDIDATES_COUNT) {
resolve(stillUrls);
return;
}
const postersUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`;
GM_xmlhttpRequest({
method: 'GET',
url: postersUrl,
headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const posters = safeGet(data, 'posters', []);
const posterUrls = posters.slice(1).map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.STILL_SIZE}/${img.file_path}`).filter(Boolean);
stillUrls = [...stillUrls, ...posterUrls].slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT);
// 去重处理
stillUrls = removeDuplicateUrls(stillUrls);
resolve(stillUrls);
} catch (e) {
resolve(stillUrls);
}
},
onerror: () => resolve(stillUrls),
ontimeout: () => resolve(stillUrls)
});
} catch (e) {
resolve(stillUrls);
}
},
onerror: () => resolve(stillUrls),
ontimeout: () => resolve(stillUrls)
});
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([]),
ontimeout: () => resolve([])
});
});
}
// 搜索相关函数
function searchDouban(query) {
return new Promise((resolve) => {
const url = `https://search.douban.com/movie/subject_search?search_text=${encodeURIComponent(query)}&cat=1002`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: { ...COMMON_HEADERS, 'Referer': 'https://movie.douban.com/', 'Host': 'search.douban.com' },
timeout: 8000,
onload: (res) => {
try {
const html = res.responseText;
const dataMatch = html.match(/window\.__DATA__\s*=\s*({.*?});/s);
if (!dataMatch || !dataMatch[1]) {
resolve([]);
return;
}
const cleanData = dataMatch[1].replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) =>
String.fromCharCode(parseInt(hex, 16))
);
const doubanData = JSON.parse(cleanData);
const items = safeGet(doubanData, 'items', []);
const results = items.map(item => ({
title: safeGet(item, 'title', '未知作品'),
type: safeGet(item, 'labels', []).some(l => l.text === '剧集') ? '电视剧' : '电影',
year: safeGet(item, 'title', '').match(/\((\d{4})\)/)?.[1] || '未知',
source: '豆瓣',
id: safeGet(item, 'id', ''),
url: safeGet(item, 'url', ''),
poster: safeGet(item, 'cover_url', '')
})).filter(item => item.url);
resolve(results);
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([]),
ontimeout: () => resolve([])
});
});
}
function searchTMDB(query) {
return new Promise((resolve) => {
const searchUrl = `${TMDB_CONFIG.BASE_URL}/search/multi?api_key=${TMDB_CONFIG.API_KEY}&query=${encodeURIComponent(query)}&language=zh-CN`;
GM_xmlhttpRequest({
method: 'GET',
url: searchUrl,
headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
timeout: 8000,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const results = safeGet(data, 'results', []).map(item => ({
title: item.title || item.name || '未知作品',
type: item.media_type === 'movie' ? '电影' : item.media_type === 'tv' ? '电视剧' : '未知',
year: item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0] || '未知',
source: 'TMDB',
id: item.id || '',
url: item.media_type === 'movie'
? `https://www.themoviedb.org/movie/${item.id}`
: item.media_type === 'tv'
? `https://www.themoviedb.org/tv/${item.id}`
: '',
poster: item.poster_path
? `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${item.poster_path}`
: ''
})).filter(item => item.url);
resolve(results);
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([]),
ontimeout: () => resolve([])
});
});
}
// 搜索结果交互
function setupSearchInteractions() {
const searchInput = document.getElementById('search-movie');
const resultsContainer = document.getElementById('search-results');
const loadingIndicator = document.getElementById('search-loading');
const mediaUrlInput = document.getElementById('media-url');
let lazyLoadController = new AbortController();
let lastSearchQuery = '';
let lastSearchResults = [];
if (!searchInput || !resultsContainer || !loadingIndicator || !mediaUrlInput) return;
searchInput.addEventListener('input', debounce(async function () {
const query = this.value.trim();
lastSearchQuery = query;
if (!query) {
resultsContainer.style.display = 'none';
loadingIndicator.style.display = 'none';
lastSearchResults = [];
abortLazyLoad();
return;
}
loadingIndicator.style.display = 'block';
resultsContainer.style.display = 'none';
abortLazyLoad();
lazyLoadController = new AbortController();
try {
const searchPromise = Promise.all([searchDouban(query), searchTMDB(query)]);
const [doubanResults, tmdbResults] = await Promise.race([
searchPromise,
new Promise(resolve => setTimeout(() => resolve([[], []]), 8000))
]);
loadingIndicator.style.display = 'none';
const allResults = [...doubanResults, ...tmdbResults];
const uniqueResults = allResults.filter((item, index, self) =>
index === self.findIndex(t => t.title === item.title && t.year === item.year)
);
lastSearchResults = uniqueResults;
if (uniqueResults.length === 0) {
resultsContainer.innerHTML = '<div style="padding:6px; color:#6b7280;">未找到结果,请尝试其他关键词</div>';
resultsContainer.style.display = 'block';
return;
}
let resultHtml = '<div class="results-container">';
uniqueResults.forEach((item, index) => {
const isDouban = item.source === '豆瓣';
const isTMDB = item.source === 'TMDB';
resultHtml += `
<div class="search-item" data-url="${item.url}" data-type="${item.type}" data-index="${index}" style="padding:6px; cursor:pointer; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; gap:6px; ${isDouban ? 'background:#eff6ff;' : isTMDB ? 'background:#e0f2fe;' : ''}">
<div class="poster-placeholder" style="width:36px; height:54px; background:#f3f4f6; border-radius:3px; display:flex; align-items:center; justify-content:center; color:#9ca3af;">
<i class="fa fa-film" style="font-size:14px;"></i>
</div>
<div>
<div><strong>${item.title}</strong> ${isDouban ? '<span style="background:#3b82f6; color:white; font-size:9px; padding:1px 2px; border-radius:2px;">豆瓣</span>' : isTMDB ? '<span style="background:#0ea5e9; color:white; font-size:9px; padding:1px 2px; border-radius:2px;">TMDB</span>' : ''}</div>
<div style="font-size:11px; color:#6b7280;">${item.type} · ${item.year} · ${item.source}</div>
</div>
</div>
`;
});
resultHtml += '</div>';
resultsContainer.innerHTML = resultHtml;
resultsContainer.style.display = 'block';
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const item = entry.target;
const index = parseInt(item.getAttribute('data-index'));
const resultItem = uniqueResults[index];
if (resultItem.poster && !lazyLoadController.signal.aborted) {
getImageDataURLWithQuality(resultItem.poster).then(dataUrl => {
if (lazyLoadController.signal.aborted) return;
const placeholder = item.querySelector('.poster-placeholder');
if (placeholder) {
placeholder.innerHTML = `<img src="${dataUrl}" style="width:36px; height:54px; object-fit:cover; border-radius:3px;" alt="${resultItem.title}">`;
}
});
}
observer.unobserve(item);
}
});
}, { rootMargin: '0px 0px 150px 0px' });
document.querySelectorAll('.search-item').forEach(item => {
observer.observe(item);
});
} catch (e) {
loadingIndicator.style.display = 'none';
resultsContainer.innerHTML = '<div style="padding:6px; color:#6b7280;">搜索出错,请重试</div>';
resultsContainer.style.display = 'block';
}
}, 500));
function abortLazyLoad() {
if (lazyLoadController) {
lazyLoadController.abort();
}
}
searchInput.addEventListener('blur', () => {
const hideTimeout = setTimeout(() => {
resultsContainer.style.display = 'none';
}, 200);
searchInput.dataset.hideTimeout = hideTimeout;
});
searchInput.addEventListener('focus', function () {
const existingTimeout = this.dataset.hideTimeout;
if (existingTimeout) {
clearTimeout(existingTimeout);
delete this.dataset.hideTimeout;
}
const query = this.value.trim();
if (query) {
resultsContainer.style.display = 'block';
if (lastSearchResults.length === 0 || lastSearchQuery !== query) {
const inputEvent = new Event('input', { bubbles: true });
this.dispatchEvent(inputEvent);
}
}
});
resultsContainer.addEventListener('click', async function (event) {
const targetItem = event.target.closest('.search-item');
if (targetItem) {
const url = targetItem.getAttribute('data-url');
const type = targetItem.getAttribute('data-type');
const title = targetItem.querySelector('strong').textContent;
if (url) {
mediaUrlInput.value = url;
resultsContainer.style.display = 'none';
searchInput.blur();
const fetchBtn = document.getElementById('fetch-btn');
if (fetchBtn) fetchBtn.style.display = 'none';
showStatus(`正在加载【${type}】${title}的信息...`, false);
try {
currentMovieInfo = await getBasicInfo(url);
currentComments = await getHotComments(url);
showStatus('信息加载完成,请选择海报和剧照', false);
await showImageSelection(currentMovieInfo);
} catch (err) {
showStatus(`加载失败:${err.message}`, true);
if (mediaUrlInput.value.trim() && fetchBtn) {
fetchBtn.style.display = 'inline-block';
}
}
}
}
});
document.addEventListener('click', function (event) {
if (!searchInput.contains(event.target) && !resultsContainer.contains(event.target)) {
resultsContainer.style.display = 'none';
}
});
}
// 影视信息提取
function getBasicInfo(url) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
if (url.includes('douban.com')) {
let isTv = url.includes('tv.douban.com');
const headers = {
...COMMON_HEADERS,
'Referer': 'https://movie.douban.com/',
'Host': urlObj.hostname
};
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: headers,
timeout: 10000,
onload: async (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const titleElem = doc.querySelector('h1 span[property="v:itemreviewed"], h1 span[itemprop="name"]');
const title = titleElem ? titleElem.textContent.trim() : (isTv ? '未知电视剧' : '未知电影');
const genreTags = Array.from(doc.querySelectorAll('span[property="v:genre"]')).map(g => g.textContent.trim()).filter(Boolean);
let year = '未知';
const yearElem = doc.querySelector('span[property="v:initialReleaseDate"]');
if (yearElem) {
const yearMatch = yearElem.textContent.trim().match(/\d{4}/);
year = yearMatch ? yearMatch[0] : '未知';
}
const alsoKnown = doc.querySelector('span[property="v:alternative"]')?.textContent.trim() || '';
const director = Array.from(doc.querySelectorAll('a[rel="v:directedBy"]')).map(d => d.textContent.trim()).join(' / ') || '未知';
const writer = Array.from(doc.querySelectorAll('.attrs')[1]?.querySelectorAll('a') || []).map(w => w.textContent.trim()).join(' / ') || '未知';
const actor = Array.from(doc.querySelectorAll('a[rel="v:starring"]')).map(a => a.textContent.trim()).slice(0, 6).join(',') || '未知';
let region = '未知';
const infoLis = doc.querySelectorAll('#info li');
Array.from(infoLis).some(li => {
const text = li.textContent.trim();
if (text.startsWith('制片国家/地区') || text.startsWith('地区')) {
region = text.split(/[::]/)[1]?.trim() || '未知';
return true;
}
return false;
});
let lang = '未知';
Array.from(infoLis).some(li => {
const text = li.textContent.trim();
if (text.startsWith('语言')) {
lang = text.split(/[::]/)[1]?.trim() || '未知';
return true;
}
return false;
});
const release = yearElem?.textContent.trim() || (isTv ? '未知首播时间' : '未知上映时间');
const rating = doc.querySelector('strong[property="v:average"]')?.textContent || '暂无';
const doubanId = url.match(/subject\/(\d+)/)?.[1] || '未知';
const imdbId = doc.querySelector('a[href*="imdb.com/title/"]')?.href?.match(/tt\d+/)?.[0] || '暂无';
const runtime = isTv
? doc.querySelector('span[property="v:episodeCount"]')
? `共${doc.querySelector('span[property="v:episodeCount"]').textContent}集`
: '未知集数'
: doc.querySelector('span[property="v:runtime"]')?.textContent.trim() || '未知片长';
const intro = doc.querySelector('span[property="v:summary"]')?.textContent.trim().replace(/\s+/g, ' ') ||
(isTv ? '暂无电视剧简介' : '暂无电影简介');
const posterUrls = await getDoubanOfficialPosters(url);
const stillUrls = await getDoubanStillsList(url);
resolve({
mediaType: isTv ? 'tv' : 'movie',
source: '豆瓣',
title, genreTags, year, alsoKnown, director, writer, actor,
region, release, lang, rating, doubanId, imdbId, runtime, intro,
posterUrls,
stillUrls,
url // 保存原始URL用于后续加载更多
});
} catch (e) {
reject(new Error(`豆瓣解析失败:${e.message}`));
}
},
onerror: () => reject(new Error('豆瓣请求失败')),
ontimeout: () => reject(new Error('豆瓣请求超时'))
});
} else if (url.includes('themoviedb.org')) {
const isMovie = url.includes('/movie/');
const isTv = url.includes('/tv/');
let mediaType = isMovie ? 'movie' : (isTv ? 'tv' : 'movie');
const idMatch = url.match(/\/(movie|tv)\/(\d+)/);
if (!idMatch) {
reject(new Error('TMDB链接格式错误(需包含/movie/或/tv/及数字ID)'));
return;
}
const [, type, id] = idMatch;
mediaType = type;
const tmdbDetailUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}?api_key=${TMDB_CONFIG.API_KEY}&language=zh-CN&append_to_response=credits`;
GM_xmlhttpRequest({
method: 'GET',
url: tmdbDetailUrl,
headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: async (res) => {
try {
const data = JSON.parse(res.responseText);
const title = mediaType === 'movie' ? data.title : data.name;
const genreTags = data.genres.map(g => g.name);
const year = mediaType === 'movie'
? data.release_date?.split('-')[0]
: data.first_air_date?.split('-')[0];
const alsoKnown = data.also_known_as?.join(' / ') || '';
const director = mediaType === 'movie'
? (data.credits?.crew?.filter(c => c.job === 'Director').map(d => d.name).join(' / ') || '未知')
: '未知';
const writer = '未知';
const actor = (data.credits?.cast || []).slice(0, 6).map(a => a.name).join(',') || '未知';
const region = (data.production_countries || []).map(c => c.name).join('、') || '未知';
const release = mediaType === 'movie' ? data.release_date : data.first_air_date;
const lang = (data.spoken_languages || []).map(l => l.name).join('、') || '未知';
const rating = data.vote_average || '暂无';
const tmdbId = id;
const imdbId = data.imdb_id || '暂无';
const runtime = mediaType === 'movie'
? `${data.runtime}分钟`
: `${data.number_of_episodes || '未知'}集(共${data.number_of_seasons || '未知'}季)`;
const intro = data.overview || (mediaType === 'tv' ? '暂无电视剧简介' : '暂无电影简介');
let posterUrls = [];
if (data.poster_path) {
posterUrls.push(`${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${data.poster_path}`);
const postersUrl = `${TMDB_CONFIG.BASE_URL}/${mediaType}/${id}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`;
await new Promise(resolvePosters => {
GM_xmlhttpRequest({
method: 'GET',
url: postersUrl,
headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => {
try {
const posterData = JSON.parse(res.responseText);
const additionalPosters = safeGet(posterData, 'posters', [])
.slice(1, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT)
.map(img => `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${img.file_path}`)
.filter(Boolean);
posterUrls = [...posterUrls, ...additionalPosters].slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT);
// 去重处理
posterUrls = removeDuplicateUrls(posterUrls);
} catch (e) {
console.log('获取更多海报失败:', e);
}
resolvePosters();
},
onerror: () => resolvePosters(),
ontimeout: () => resolvePosters()
});
});
}
const stillUrls = await getTMDBStillsList(mediaType, id);
resolve({
mediaType,
source: 'TMDB',
title, genreTags, year, alsoKnown, director, writer, actor,
region, release, lang, rating, tmdbId, imdbId, runtime, intro,
posterUrls,
stillUrls,
url // 保存原始URL用于后续加载更多
});
} catch (e) {
reject(new Error(`TMDB解析失败:${e.message}`));
}
},
onerror: () => reject(new Error('TMDB请求失败')),
ontimeout: () => reject(new Error('TMDB请求超时'))
});
} else {
reject(new Error('不支持的链接类型(仅支持豆瓣、TMDB)'));
}
});
}
function getHotComments(url) {
return new Promise(resolve => {
if (url.includes('themoviedb.org')) {
resolve([]);
return;
}
const commentUrl = url.replace(/\/subject\/(\d+)\/?$/, '/subject/$1/comments?status=P');
GM_xmlhttpRequest({
method: 'GET',
url: commentUrl,
headers: { ...COMMON_HEADERS, 'Referer': url, 'Host': new URL(url).hostname },
timeout: 8000,
onload: (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const comments = Array.from(doc.querySelectorAll('.comment-item .short'))
.slice(0, 3)
.map(elem => elem.textContent.trim())
.filter(Boolean);
resolve(comments.length ? comments : []);
} catch (e) {
resolve([]);
}
},
onerror: () => resolve([]),
ontimeout: () => resolve([])
});
});
}
// 加载更多海报功能(每组5张,滚轮触发,去重优化)
async function loadMorePosters() {
if (isLoadingPosters) {
showStatus('正在加载海报,请稍候...', false);
return;
}
if (!currentMovieInfo) {
showStatus('未找到影视信息,请重新加载', true);
return;
}
isLoadingPosters = true;
const loadMorePostersBtn = document.getElementById('load-more-posters');
if (loadMorePostersBtn) {
loadMorePostersBtn.textContent = '加载中...';
loadMorePostersBtn.disabled = true;
}
try {
// 首次加载所有海报(带唯一标识)并去重
if (!window.allPostersWithIds) {
window.allPostersWithIds = [];
if (currentMovieInfo.source === '豆瓣') {
const rawPosters = await getDoubanOfficialPosters(currentMovieInfo.url);
window.allPostersWithIds = rawPosters.map(url => ({
url,
id: url // 用URL作为唯一标识
}));
} else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) {
const postersUrl = `${TMDB_CONFIG.BASE_URL}/${currentMovieInfo.mediaType}/${currentMovieInfo.tmdbId}/images?api_key=${TMDB_CONFIG.API_KEY}&include_image_language=zh,en&image_type=poster&sort_by=primary`;
await new Promise(resolvePosters => {
GM_xmlhttpRequest({
method: 'GET',
url: postersUrl,
headers: { 'Authorization': `Bearer ${TMDB_CONFIG.ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
onload: (res) => {
try {
const posterData = JSON.parse(res.responseText);
window.allPostersWithIds = safeGet(posterData, 'posters', []).map(img => ({
url: `${TMDB_CONFIG.IMAGE_BASE_URL}${TMDB_CONFIG.POSTER_SIZE}/${img.file_path}`,
id: img.file_path // 用TMDB的file_path作为唯一标识
})).filter(img => img.url);
// 去重处理(通过id)
const uniquePosters = [];
const idSet = new Set();
window.allPostersWithIds.forEach(img => {
if (!idSet.has(img.id)) {
idSet.add(img.id);
uniquePosters.push(img);
}
});
window.allPostersWithIds = uniquePosters;
} catch (e) {
console.log('获取TMDB海报列表失败:', e);
window.allPostersWithIds = [];
}
resolvePosters();
},
onerror: () => {
window.allPostersWithIds = [];
resolvePosters();
},
ontimeout: () => {
window.allPostersWithIds = [];
resolvePosters();
}
});
});
} else {
window.allPostersWithIds = [];
}
}
const allPosters = window.allPostersWithIds || [];
// 每组5张,计算当前组的起始和结束索引
const startIndex = posterPage * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT;
const endIndex = (posterPage + 1) * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT;
const postersToLoad = allPosters.slice(startIndex, endIndex);
if (postersToLoad.length > 0) {
if (posterContainer) {
posterContainer.style.display = 'grid';
posterContainer.style.gridTemplateColumns = 'repeat(5, 1fr)'; // 每行5张,排版整齐
posterContainer.style.gap = '8px'; // 图片间距,避免遮挡
for (let i = 0; i < postersToLoad.length; i++) {
const poster = postersToLoad[i];
if (loadedPosterIds.has(poster.id)) {
continue; // 跳过已加载的海报(去重)
}
loadedPosterIds.add(poster.id); // 标记为已加载
try {
const dataUrl = await getImageDataURLWithQuality(poster.url);
const posterImg = document.createElement('div');
posterImg.style.cssText = `
width: 150px;
height: 190px;
flex-shrink: 0;
border: 1px solid #e5e7eb;
border-radius: 4px;
cursor: pointer;
overflow: hidden;
background: #f9fafb;
display: flex;
align-items: center;
justify-content: center;
`;
posterImg.innerHTML = `<img src="${dataUrl}" style="width:100%; height:100%; object-fit: contain;" alt="海报 ${startIndex + i + 1}">`;
posterImg.addEventListener('click', function () {
selectedPosterUrl = dataUrl;
document.querySelectorAll('#poster-candidates > div').forEach(el => el.style.border = '1px solid #e5e7eb');
this.style.border = '3px solid #3b82f6';
});
posterContainer.appendChild(posterImg);
} catch (e) {
console.log(`加载海报 ${startIndex + i + 1} 失败:`, e);
}
}
// 滚动到容器底部,显示新增海报
posterContainer.scrollTop = posterContainer.scrollHeight;
// 页码递增,准备下一组加载
posterPage++;
showStatus(`已加载第${posterPage}组海报`, false);
}
} else {
showStatus('已加载全部海报', false);
}
} catch (e) {
console.error('加载更多海报出错:', e);
showStatus('加载更多海报失败,请稍后重试', true);
} finally {
if (loadMorePostersBtn) {
loadMorePostersBtn.textContent = '加载更多海报';
loadMorePostersBtn.disabled = false;
}
isLoadingPosters = false;
}
}
// 加载更多剧照功能(每组5张,滚轮触发,去重优化)
async function loadMoreStills() {
if (isLoadingStills) {
showStatus('正在加载剧照,请稍候...', false);
return;
}
if (!currentMovieInfo) {
showStatus('未找到影视信息,请重新加载', true);
return;
}
isLoadingStills = true;
const loadMoreStillsBtn = document.getElementById('load-more-stills');
if (loadMoreStillsBtn) {
loadMoreStillsBtn.textContent = '加载中...';
loadMoreStillsBtn.disabled = true;
}
try {
// 首次加载所有剧照(带唯一标识)并去重
if (!window.allStillsWithIds) {
window.allStillsWithIds = [];
if (currentMovieInfo.source === '豆瓣') {
const rawStills = await getDoubanStillsList(currentMovieInfo.url);
window.allStillsWithIds = rawStills.map(url => ({
url,
id: url // 用URL作为唯一标识
}));
} else if (currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId) {
const allStillsUrls = await getTMDBStillsList(currentMovieInfo.mediaType, currentMovieInfo.tmdbId);
window.allStillsWithIds = allStillsUrls.map(url => ({
url,
id: url // 用URL作为唯一标识
}));
} else {
window.allStillsWithIds = [];
}
// 去重处理(通过id)
const uniqueStills = [];
const idSet = new Set();
window.allStillsWithIds.forEach(img => {
if (!idSet.has(img.id)) {
idSet.add(img.id);
uniqueStills.push(img);
}
});
window.allStillsWithIds = uniqueStills;
}
const allStills = window.allStillsWithIds || [];
// 每组5张,计算当前组的起始和结束索引
const startIndex = stillPage * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT;
const endIndex = (stillPage + 1) * TMDB_CONFIG.IMAGE_CANDIDATES_COUNT;
const stillsToLoad = allStills.slice(startIndex, endIndex);
if (stillsToLoad.length > 0) {
if (stillContainer) {
stillContainer.style.display = 'grid';
stillContainer.style.gridTemplateColumns = 'repeat(5, 1fr)'; // 每行5张,排版整齐
stillContainer.style.gap = '8px'; // 图片间距,避免遮挡
for (let i = 0; i < stillsToLoad.length; i++) {
const still = stillsToLoad[i];
if (loadedStillIds.has(still.id)) {
continue; // 跳过已加载的剧照(去重)
}
loadedStillIds.add(still.id); // 标记为已加载
try {
const dataUrl = await getImageDataURLWithQuality(still.url);
const stillImg = document.createElement('div');
stillImg.style.cssText = `
width: 200px;
height: 110px;
flex-shrink: 0;
border: 1px solid #e5e7eb;
border-radius: 4px;
cursor: pointer;
overflow: hidden;
background: #f9fafb;
display: flex;
align-items: center;
justify-content: center;
`;
stillImg.innerHTML = `<img src="${dataUrl}" style="width:100%; height:100%; object-fit: contain;" alt="剧照 ${startIndex + i + 1}">`;
stillImg.addEventListener('click', function () {
selectedStillUrl = dataUrl;
document.querySelectorAll('#still-candidates > div').forEach(el => el.style.border = '1px solid #e5e7eb');
this.style.border = '3px solid #3b82f6';
});
stillContainer.appendChild(stillImg);
} catch (e) {
console.log(`加载剧照 ${startIndex + i + 1} 失败:`, e);
}
}
// 滚动到容器底部,显示新增剧照
stillContainer.scrollTop = stillContainer.scrollHeight;
// 页码递增,准备下一组加载
stillPage++;
showStatus(`已加载第${stillPage}组剧照`, false);
}
} else {
showStatus('已加载全部剧照', false);
if (loadMoreStillsBtn) {
loadMoreStillsBtn.textContent = '没有更多剧照了';
loadMoreStillsBtn.disabled = true;
loadMoreStillsBtn.style.opacity = '0.6';
}
}
} catch (e) {
console.error('加载更多剧照出错:', e);
showStatus('加载更多剧照失败,请稍后重试', true);
if (loadMoreStillsBtn) {
loadMoreStillsBtn.textContent = '加载失败,重试';
loadMoreStillsBtn.disabled = false;
}
} finally {
if (loadMoreStillsBtn) {
loadMoreStillsBtn.textContent = '加载更多剧照';
loadMoreStillsBtn.disabled = false;
}
isLoadingStills = false;
}
}
async function showImageSelection(movieInfo) {
return new Promise(async (resolve) => {
if (!posterContainer || !stillContainer) {
posterContainer = document.getElementById('poster-candidates');
stillContainer = document.getElementById('still-candidates');
}
const imageSelection = document.getElementById('image-selection');
const loadMorePostersBtn = document.getElementById('load-more-posters');
const loadMoreStillsBtn = document.getElementById('load-more-stills');
loadedPosterIds.clear(); // 清空已加载海报标记,重新开始
posterPage = 0; // 重置海报分页
loadedStillIds.clear(); // 清空已加载剧照标记,重新开始
stillPage = 0; // 重置剧照分页
if (!posterContainer || !stillContainer || !imageSelection) {
resolve();
return;
}
posterContainer.style.display = 'grid';
posterContainer.innerHTML = '<div style="color:#6b7280;">加载海报中...</div>';
stillContainer.innerHTML = '<div style="color:#6b7280;">加载剧照中...</div>';
imageSelection.style.display = 'block';
loadMorePostersBtn.style.display = 'none';
loadMoreStillsBtn.style.display = 'none';
if (movieInfo.posterUrls && movieInfo.posterUrls.length > 0) {
posterContainer.style.display = 'grid';
posterContainer.innerHTML = '';
selectedPosterUrl = await getImageDataURLWithQuality(movieInfo.posterUrls[0]);
// 初始加载第一组(5张)海报,标记已加载ID
const initialPosters = movieInfo.posterUrls.slice(0, TMDB_CONFIG.IMAGE_CANDIDATES_COUNT).map(url => ({
url,
id: url
}));
for (let i = 0; i < initialPosters.length; i++) {
const poster = initialPosters[i];
loadedPosterIds.add(poster.id); // 标记初始海报为已加载
try {
const dataUrl = await getImageDataURLWithQuality(poster.url);
const posterImg = document.createElement('div');
posterImg.style.cssText = `
width: 150px; height: 190px; flex-shrink: 0;
border: ${i === 0 ? '3px solid #3b82f6' : '1px solid #e5e7eb'};
border-radius: 4px; cursor: pointer; overflow: hidden;
background: #f9fafb; display: flex; align-items: center; justify-content: center;
`;
posterImg.innerHTML = `<img src="${dataUrl}" onError="this.src='https://picsum.photos/150/190?default-poster'" style="width:100%; height:100%; object-fit: contain;" alt="海报 ${i + 1}">`;
posterImg.addEventListener('click', function () {
selectedPosterUrl = dataUrl;
document.querySelectorAll('#poster-candidates > div').forEach(el => el.style.border = '1px solid #e5e7eb');
this.style.border = '3px solid #3b82f6';
});
posterContainer.appendChild(posterImg);
} catch (e) {
console.log(`加载初始海报 ${i + 1} 失败:`, e);
}
}
// 如果有更多海报,显示“加载更多”按钮
if (movieInfo.posterUrls.length > TMDB_CONFIG.IMAGE_CANDIDATES_COUNT) {
loadMorePostersBtn.style.display = 'inline-block';
loadMorePostersBtn.disabled = false;
}
} else {
posterContainer.innerHTML = '<div style="color:#6b7280;">未找到海报</div>';
selectedPosterUrl = 'https://picsum.photos/800/450?default-poster';
}
// 剧照初始加载逻辑:只准备加载更多按钮,不自动加载
const hasStills = (movieInfo.stillUrls && movieInfo.stillUrls.length > 0) ||
(currentMovieInfo.source === '豆瓣' && currentMovieInfo.url) ||
(currentMovieInfo.source === 'TMDB' && currentMovieInfo.tmdbId);
if (hasStills) {
stillContainer.innerHTML = '<div style="color:#6b7280;">点击“加载更多剧照”查看剧照</div>';
loadMoreStillsBtn.style.display = 'inline-block';
loadMoreStillsBtn.disabled = false;
} else {
stillContainer.style.display = 'grid';
stillContainer.innerHTML = '<div style="color:#6b7280;">未找到剧照</div>';
selectedStillUrl = 'https://picsum.photos/800/450?default-still';
loadMoreStillsBtn.style.display = 'none';
}
resolve();
});
}
// 初始化美化工具
function initFormatTools() {
const buttonContainer = document.getElementById('format-buttons');
const categoryContainer = document.getElementById('format-categories');
const previewContainer = document.getElementById('format-preview');
const previewToggle = document.getElementById('format-preview-toggle');
if (!buttonContainer || !categoryContainer || !previewContainer || !previewToggle) return;
// 清空容器
buttonContainer.innerHTML = '';
categoryContainer.innerHTML = '';
// 获取所有唯一分类
const categories = [...new Set(FORMAT_STYLES.map(style => style.category))];
// 创建分类标签
categories.forEach(category => {
const catBtn = document.createElement('div');
catBtn.textContent = category;
catBtn.style.cssText = `
padding:3px 8px; background:#e2e8f0; color:#4b5563; border-radius:4px;
font-size:12px; cursor:pointer; white-space:nowrap;
`;
// 默认选中第一个分类
if (category === categories[0]) {
catBtn.style.background = '#3b82f6';
catBtn.style.color = 'white';
}
// 点击分类标签过滤样式
catBtn.addEventListener('click', () => {
// 更新分类按钮样式
document.querySelectorAll('#format-categories > div').forEach(btn => {
btn.style.background = '#e2e8f0';
btn.style.color = '#4b5563';
});
catBtn.style.background = '#3b82f6';
catBtn.style.color = 'white';
// 显示选中分类的样式按钮
document.querySelectorAll('#format-buttons > button').forEach(btn => {
const btnCategory = btn.getAttribute('data-category');
btn.style.display = btnCategory === category ? 'inline-flex' : 'none';
});
// 清空预览
previewContainer.innerHTML = '<div style="text-align:center; color:#6b7280; font-size:13px;">选择样式查看预览效果</div>';
});
categoryContainer.appendChild(catBtn);
});
// 创建样式按钮
FORMAT_STYLES.forEach((style, index) => {
const btn = document.createElement('button');
const iconHtml = style.icon ? `<i class="fa ${style.icon}" style="margin-right:4px;"></i>` : '';
btn.innerHTML = `${iconHtml}${style.name}`;
btn.setAttribute('data-category', style.category);
btn.style.cssText = `
background: #22c55e; color: white; border: none;
padding: 5px 10px; border-radius: 3px; cursor: pointer;
font-size: 12px; margin: 1px; display: ${index === 0 ? 'inline-flex' : 'none'};
align-items: center; display: ${style.category === categories[0] ? 'inline-flex' : 'none'};
`;
// 样式预览功能
if (style.preview) {
btn.addEventListener('mouseenter', () => {
if (previewContainer.style.display === 'block') {
previewContainer.innerHTML = `
<div style="margin-bottom:5px; font-size:13px; color:#4b5563; font-weight:500;">
${style.name} 预览:
</div>
<div class="style-preview-content">
${style.apply()}
</div>
`;
}
});
}
// 样式应用功能
btn.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
// 添加点击动画反馈
btn.style.background = '#166534';
setTimeout(() => {
btn.style.background = '#22c55e';
}, 200);
await autoClickSourceBtn();
const editor = getCurrentEditor();
if (!editor) {
showStatus('未找到编辑框,请先切换到源代码模式', true);
return;
}
let selectedText = '';
if (editor.type === 'codemirror') {
selectedText = editor.instance.getSelection();
} else {
selectedText = editor.instance.value.substring(
editor.instance.selectionStart,
editor.instance.selectionEnd
);
}
const styledHtml = style.apply(selectedText);
if (editor.type === 'codemirror') {
editor.instance.replaceSelection(styledHtml);
} else {
const start = editor.instance.selectionStart;
const end = editor.instance.selectionEnd;
editor.instance.value = editor.instance.value.substring(0, start) + styledHtml + editor.instance.value.substring(end);
editor.instance.dispatchEvent(new Event('input', { bubbles: true }));
editor.instance.focus();
editor.instance.setSelectionRange(start + styledHtml.length, start + styledHtml.length);
}
const saved = await autoClickSaveBtn();
if (saved) {
showStatus(`已应用“${style.name}”并自动保存`, false);
} else {
showStatus(`已应用“${style.name}”,请手动保存`, false);
}
});
buttonContainer.appendChild(btn);
});
// 预览区域切换功能
previewToggle.addEventListener('click', () => {
if (previewContainer.style.display === 'none') {
previewContainer.style.display = 'block';
previewToggle.textContent = '隐藏预览';
} else {
previewContainer.style.display = 'none';
previewToggle.textContent = '显示预览';
}
});
}
function getCurrentEditor() {
if (sourceCodeElement && sourceCodeElement.offsetParent !== null) {
return { type: 'textarea', instance: sourceCodeElement };
}
const codeMirror = document.querySelector('.CodeMirror');
if (codeMirror && codeMirror.CodeMirror) {
return { type: 'codemirror', instance: codeMirror.CodeMirror };
}
const editorSelectors = [
'#myModal-code textarea',
'textarea.tox-textarea',
'textarea.mce-textbox',
'textarea.cke_source',
'textarea[name="message"]',
'#editor_content'
];
for (const selector of editorSelectors) {
const elem = document.querySelector(selector);
if (elem && elem.style.display !== 'none') {
sourceCodeElement = elem;
return { type: 'textarea', instance: elem };
}
}
return null;
}
// 绑定按钮事件
function bindEventListeners() {
const fetchBtn = document.getElementById('fetch-btn');
const mediaUrlInput = document.getElementById('media-url');
const pasteBtn = document.getElementById('paste-btn');
const clearBtn = document.getElementById('clear-btn');
const confirmImagesBtn = document.getElementById('confirm-images-btn');
const loadMorePostersBtn = document.getElementById('load-more-posters');
const loadMoreStillsBtn = document.getElementById('load-more-stills');
if (mediaUrlInput) {
mediaUrlInput.addEventListener('input', function () {
if (fetchBtn) {
fetchBtn.style.display = this.value.trim() ? 'inline-block' : 'none';
}
});
}
if (fetchBtn) {
fetchBtn.addEventListener('click', async function () {
const url = mediaUrlInput.value.trim();
if (!url) {
showStatus('请输入影视链接', true);
return;
}
showStatus('正在提取影视信息...', false);
try {
currentMovieInfo = await getBasicInfo(url);
currentComments = await getHotComments(url);
showStatus('信息提取完成,请选择海报和剧照', false);
await showImageSelection(currentMovieInfo);
} catch (err) {
showStatus(`提取失败:${err.message || '未知错误'}`, true);
}
});
}
if (pasteBtn) {
pasteBtn.addEventListener('click', async function () {
const backupHtml = document.getElementById('backup-html').value;
if (backupHtml) {
await autoClickSourceBtn();
const filled = await autoFillSourceBox(backupHtml);
if (filled) {
showStatus('内容已粘贴到编辑框', false);
} else {
showStatus('内容粘贴失败,请手动粘贴剪贴板内容', true);
}
}
});
}
if (clearBtn) {
clearBtn.addEventListener('click', function () {
if (mediaUrlInput) mediaUrlInput.value = '';
if (document.getElementById('search-movie')) document.getElementById('search-movie').value = '';
if (document.getElementById('search-results')) document.getElementById('search-results').style.display = 'none';
if (document.getElementById('image-selection')) document.getElementById('image-selection').style.display = 'none';
if (posterContainer) posterContainer.innerHTML = '';
if (stillContainer) stillContainer.innerHTML = '';
if (fetchBtn) fetchBtn.style.display = 'none';
selectedPosterUrl = '';
selectedStillUrl = '';
currentMovieInfo = null;
currentComments = [];
posterPage = 0;
loadedPosterIds.clear(); // 清空去重标记
stillPage = 0;
loadedStillIds.clear(); // 清空剧照去重标记
showStatus('已清除所有内容', false);
});
}
if (confirmImagesBtn) {
confirmImagesBtn.addEventListener('click', async function () {
if (!currentMovieInfo) {
showStatus('未找到影视信息,请重新加载', true);
return;
}
const finalPosterUrl = selectedPosterUrl || 'https://picsum.photos/800/450?default-poster';
const finalStillUrl = selectedStillUrl || 'https://picsum.photos/800/450?default-still';
showStatus('正在生成HTML内容...', false);
const html = generateHTML(currentMovieInfo, currentComments, finalPosterUrl, finalStillUrl);
const backupHtml = document.getElementById('backup-html');
if (backupHtml) backupHtml.value = html;
const success = await fillAndSaveSource(html);
if (success) {
showStatus('内容已填充并自动保存(非发布)', false);
}
});
}
// 绑定加载更多海报按钮
if (loadMorePostersBtn) {
loadMorePostersBtn.addEventListener('click', loadMorePosters);
}
// 绑定加载更多剧照按钮
if (loadMoreStillsBtn) {
loadMoreStillsBtn.addEventListener('click', loadMoreStills);
}
// 绑定海报容器滚轮事件:点击“加载更多海报”后,滚轮可触发加载下一组
if (posterContainer) {
posterContainer.addEventListener('wheel', function (e) {
// 只有点击过“加载更多海报”(posterPage > 0)才开启滚轮加载
if (posterPage > 0 && !isLoadingPosters) {
// 向下滚动时加载下一组
if (e.deltaY > 0) {
const loadMoreBtn = document.getElementById('load-more-posters');
if (loadMoreBtn) {
loadMoreBtn.click();
}
}
}
});
}
// 绑定剧照容器滚轮事件:点击“加载更多剧照”后,滚轮可触发加载下一组
if (stillContainer) {
stillContainer.addEventListener('wheel', function (e) {
// 只有点击过“加载更多剧照”(stillPage > 0)才开启滚轮加载
if (stillPage > 0 && !isLoadingStills) {
// 向下滚动时加载下一组
if (e.deltaY > 0) {
const loadMoreBtn = document.getElementById('load-more-stills');
if (loadMoreBtn) {
loadMoreBtn.click();
}
}
}
});
}
setupSearchInteractions();
}
// 初始化页面
function init() {
// 加载Font Awesome图标
const faLink = document.createElement('link');
faLink.rel = 'stylesheet';
faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css';
document.head.appendChild(faLink);
// 插入控制面板
insertPanelInMarkedPosition();
// 检查是否有默认URL
const urlParams = new URLSearchParams(window.location.search);
const mediaUrl = urlParams.get('mediaUrl');
if (mediaUrl && document.getElementById('media-url')) {
document.getElementById('media-url').value = mediaUrl;
const fetchBtn = document.getElementById('fetch-btn');
if (fetchBtn) fetchBtn.style.display = 'inline-block';
}
}
// 启动脚本
init();
})();