// ==UserScript== // @name TMDB影视热播 // @namespace https://github.com/tmdb-dashboard // @version 1.0.0 // @description 热播剧/动漫追剧面板:国漫、美剧、韩剧、英剧、中剧等热播信息,支持搜索、地区筛选、年份筛选、今日/昨日/明日更新 // @author OpenClaw // @match *://*.baidu.com/* // @match *://*.bilibili.com/* // @match *://*.douban.com/* // @match *://*.zhihu.com/* // @match *://*.weibo.com/* // @match *://*.douyin.com/* // @match *://*.google.com/* // @match *://*.youtube.com/* // @match *://*.github.com/* // @match *://*.twitter.com/* // @match *://*.x.com/* // @match *://localhost/* // @match *://127.0.0.1/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @connect api.themoviedb.org // @connect api.tmdb.org // @connect image.tmdb.org // @connect * // @run-at document-idle // @noframes // @downloadURL https://update.greasyfork.icu/scripts/576683/TMDB%E5%BD%B1%E8%A7%86%E7%83%AD%E6%92%AD.user.js // @updateURL https://update.greasyfork.icu/scripts/576683/TMDB%E5%BD%B1%E8%A7%86%E7%83%AD%E6%92%AD.meta.js // ==/UserScript== (function () { 'use strict'; // ═══════════════════════════════════════════════════ // 配置 // ═══════════════════════════════════════════════════ const CONFIG = { API_KEY: GM_getValue('tmdb_api_key', ''), API_HOSTS: ['https://api.themoviedb.org/3', 'https://api.tmdb.org/3'], IMG_BASE: 'https://image.tmdb.org/t/p/', LANGUAGE: 'zh-CN', PAGE_SIZE: 20, REQUEST_TIMEOUT: 12000, PROXY_HOST: GM_getValue('tmdb_proxy_host', ''), // 地区筛选选项(用于今日/昨日/明日更新) REGIONS: [ { code: '', label: '全部' }, { code: 'CN|HK|TW', label: '🇨🇳 中国' }, { code: 'US', label: '🇺🇸 美国' }, { code: 'KR', label: '🇰🇷 韩国' }, { code: 'JP', label: '🇯🇵 日本' }, { code: 'GB', label: '🇬🇧 英国' }, { code: 'TH', label: '🇹🇭 泰国' }, { code: 'RU', label: '🇷🇺 俄罗斯' }, { code: 'IN', label: '🇮🇳 印度' }, { code: 'FR', label: '🇫🇷 法国' }, { code: 'DE', label: '🇩🇪 德国' }, { code: 'CA', label: '🇨🇦 加拿大' }, { code: 'AU', label: '🇦🇺 澳大利亚' }, ], // 年份选项(用于热播剧) YEARS: (() => { const y = new Date().getFullYear(); const arr = [{ value: '', label: '全部年份' }]; for (let i = y; i >= 2000; i--) arr.push({ value: String(i), label: String(i) }); return arr; })(), CATEGORIES: [ { id: 'trending', label: '🔥 热播总榜', type: 'trending', filter: 'year' }, { id: 'cn_drama', label: '🇨🇳 中剧', type: 'discover', filter: 'year', with_origin_country: 'CN' }, { id: 'cn_anime', label: '🐉 国漫', type: 'discover', filter: 'year', with_origin_country: 'CN', with_genres: '16' }, { id: 'us_drama', label: '🇺🇸 美剧', type: 'discover', filter: 'year', with_origin_country: 'US' }, { id: 'kr_drama', label: '🇰🇷 韩剧', type: 'discover', filter: 'year', with_origin_country: 'KR' }, { id: 'uk_drama', label: '🇬🇧 英剧', type: 'discover', filter: 'year', with_origin_country: 'GB' }, { id: 'jp_drama', label: '🇯🇵 日剧', type: 'discover', filter: 'year', with_origin_country: 'JP', without_genres: '16' }, { id: 'jp_anime', label: '🌸 日漫', type: 'discover', filter: 'year', with_origin_country: 'JP', with_genres: '16' }, { id: 'today', label: '📅 今日更新', type: 'airing_today', filter: 'region' }, { id: 'yesterday', label: '⏪ 昨日更新', type: 'airing_date', filter: 'region', offset: -1 }, { id: 'tomorrow', label: '⏩ 明日更新', type: 'airing_date', filter: 'region', offset: 1 }, ], }; // ═══════════════════════════════════════════════════ // 网络层 // ═══════════════════════════════════════════════════ const log = (...args) => console.log('[热播剧面板]', ...args); let workingHost = GM_getValue('tmdb_working_host', ''); function getHosts() { const hosts = []; if (CONFIG.PROXY_HOST) { const h = CONFIG.PROXY_HOST.replace(/\/+$/, ''); if (!hosts.includes(h)) hosts.push(h); } if (workingHost && !hosts.includes(workingHost)) hosts.push(workingHost); for (const h of CONFIG.API_HOSTS) { if (!hosts.includes(h)) hosts.push(h); } return hosts; } function tryFetch(path, params = {}) { const qs = new URLSearchParams(); qs.set('api_key', CONFIG.API_KEY); qs.set('language', CONFIG.LANGUAGE); for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== null && v !== '') qs.set(k, v); } const query = qs.toString(); const hosts = getHosts(); return new Promise((resolve, reject) => { let hostIdx = 0; function tryNext() { if (hostIdx >= hosts.length) { reject(new Error('所有 API 地址均不可用')); return; } const baseUrl = hosts[hostIdx]; const url = `${baseUrl}${path}?${query}`; log(`尝试 [${hostIdx + 1}/${hosts.length}]:`, url); const timer = setTimeout(() => { hostIdx++; tryNext(); }, CONFIG.REQUEST_TIMEOUT); GM_xmlhttpRequest({ method: 'GET', url, responseType: 'json', timeout: CONFIG.REQUEST_TIMEOUT, onload(res) { clearTimeout(timer); if (res.status === 401) { reject(new Error('API Key 无效')); return; } if (res.status === 429) { reject(new Error('请求过于频繁')); return; } if (res.status >= 200 && res.status < 300) { if (!workingHost || workingHost !== baseUrl) { workingHost = baseUrl; GM_setValue('tmdb_working_host', baseUrl); } let data = res.response; if (!data || typeof data !== 'object') { try { data = JSON.parse(res.responseText); } catch(e) { reject(new Error('JSON 解析失败')); return; } } resolve(data); } else { hostIdx++; tryNext(); } }, onerror() { clearTimeout(timer); hostIdx++; tryNext(); }, ontimeout() { clearTimeout(timer); hostIdx++; tryNext(); }, }); } tryNext(); }); } // ═══════════════════════════════════════════════════ // 状态 // ═══════════════════════════════════════════════════ let currentTab = CONFIG.CATEGORIES[0].id; let currentPage = 1; let cachedData = {}; let showSettings = false; // 筛选状态 let filterRegion = ''; // 地区筛选(今日/昨日/明日更新用) let filterYear = ''; // 年份筛选(热播剧用) let searchQuery = ''; // 搜索关键词 let searchTimer = null; // ═══════════════════════════════════════════════════ // 日期工具 // ═══════════════════════════════════════════════════ function formatDate(d) { return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; } function getRelativeDate(offset) { const d = new Date(); d.setDate(d.getDate() + offset); return formatDate(d); } // ═══════════════════════════════════════════════════ // 数据获取 // ═══════════════════════════════════════════════════ async function fetchCategory(cat, page = 1, opts = {}) { switch (cat.type) { case 'trending': { const params = { page }; if (opts.year) { // trending 不支持年份筛选,用 discover 代替 return (await tryFetch('/discover/tv', { page, sort_by: 'popularity.desc', 'vote_count.gte': 10, 'first_air_date.gte': `${opts.year}-01-01`, 'first_air_date.lte': `${opts.year}-12-31`, })).results || []; } return (await tryFetch('/trending/tv/week', params)).results || []; } case 'discover': { const params = { page, sort_by: 'popularity.desc', 'vote_count.gte': 10 }; if (cat.with_origin_country) params.with_origin_country = cat.with_origin_country; if (cat.with_genres) params.with_genres = cat.with_genres; if (cat.without_genres) params.without_genres = cat.without_genres; if (opts.year) { params['first_air_date.gte'] = `${opts.year}-01-01`; params['first_air_date.lte'] = `${opts.year}-12-31`; } return (await tryFetch('/discover/tv', params)).results || []; } case 'airing_today': { if (opts.country) { // 用 discover + air_date 筛选特定地区 const today = formatDate(new Date()); return (await tryFetch('/discover/tv', { page, sort_by: 'popularity.desc', 'air_date.gte': today, 'air_date.lte': today, with_origin_country: opts.country, })).results || []; } return (await tryFetch('/tv/airing_today', { page })).results || []; } case 'airing_date': { const date = getRelativeDate(cat.offset); const params = { page, sort_by: 'popularity.desc', 'air_date.gte': date, 'air_date.lte': date }; if (opts.country) params.with_origin_country = opts.country; return (await tryFetch('/discover/tv', params)).results || []; } default: return []; } } async function searchTV(query, page = 1) { return (await tryFetch('/search/tv', { query, page })).results || []; } async function fetchTVDetail(id) { return tryFetch(`/tv/${id}`); } // ═══════════════════════════════════════════════════ // CSS // ═══════════════════════════════════════════════════ GM_addStyle(` #tmdb-panel-root { position: fixed; top: 0; right: 0; width: 920px; height: 100vh; z-index: 2147483647; font-family: -apple-system, 'Segoe UI', Roboto, 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: #0d1117; color: #e6edf3; overflow: hidden; box-shadow: -4px 0 24px rgba(0,0,0,.5); transform: translateX(100%); transition: transform .35s cubic-bezier(.4,0,.2,1); display: flex; flex-direction: column; } #tmdb-panel-root.open { transform: translateX(0); } #tmdb-fab { position: fixed; top: 50%; right: 0; transform: translateY(-50%); z-index: 2147483647; width: 42px; height: 120px; background: linear-gradient(135deg, #1e40af, #7c3aed); border: none; border-radius: 12px 0 0 12px; cursor: pointer; color: #fff; font-size: 13px; font-weight: 700; writing-mode: vertical-rl; letter-spacing: 4px; display: flex; align-items: center; justify-content: center; box-shadow: -2px 0 12px rgba(124,58,237,.4); transition: width .2s; } #tmdb-fab:hover { width: 48px; } .tmdb-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; background: linear-gradient(135deg, #161b22, #0d1117); border-bottom: 1px solid #30363d; flex-shrink: 0; } .tmdb-header h2 { margin: 0; font-size: 18px; color: #58a6ff; } .tmdb-close, .tmdb-settings-btn { background: none; border: none; color: #8b949e; font-size: 20px; cursor: pointer; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: all .15s; } .tmdb-close:hover, .tmdb-settings-btn:hover { background: #30363d; color: #e6edf3; } .tmdb-api-bar { display: flex; align-items: center; gap: 10px; padding: 12px 20px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; } .tmdb-api-bar label { font-size: 12px; color: #8b949e; white-space: nowrap; } .tmdb-input { flex: 1; padding: 8px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 13px; outline: none; min-width: 0; } .tmdb-input:focus { border-color: #58a6ff; } .tmdb-btn { padding: 8px 16px; background: #238636; border: none; border-radius: 6px; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; } .tmdb-btn:hover { background: #2ea043; } .tmdb-btn-secondary { padding: 8px 16px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 13px; cursor: pointer; white-space: nowrap; } .tmdb-btn-secondary:hover { background: #30363d; } .tmdb-api-status { font-size: 12px; color: #8b949e; min-width: 60px; } .tmdb-settings { padding: 16px 20px; background: #161b22; border-bottom: 1px solid #30363d; } .tmdb-settings h4 { margin: 0 0 12px; font-size: 14px; color: #58a6ff; } .tmdb-settings-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .tmdb-settings-row label { font-size: 12px; color: #8b949e; min-width: 100px; } .tmdb-settings-row input { flex: 1; padding: 6px 10px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 12px; outline: none; } .tmdb-settings-hint { font-size: 11px; color: #484f58; margin-top: 4px; } /* 搜索栏 */ .tmdb-search-bar { display: flex; align-items: center; gap: 8px; padding: 10px 20px; background: #0d1117; border-bottom: 1px solid #21262d; flex-shrink: 0; } .tmdb-search-bar .tmdb-search-icon { color: #484f58; font-size: 16px; flex-shrink: 0; } .tmdb-search-bar input { flex: 1; padding: 8px 12px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; color: #e6edf3; font-size: 14px; outline: none; } .tmdb-search-bar input:focus { border-color: #58a6ff; background: #1c2129; } .tmdb-search-bar input::placeholder { color: #484f58; } .tmdb-search-clear { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 16px; padding: 4px; border-radius: 4px; display: none; } .tmdb-search-clear:hover { color: #e6edf3; background: #30363d; } .tmdb-search-clear.show { display: block; } /* Tab 栏 */ .tmdb-tabs { display: flex; gap: 0; overflow-x: auto; padding: 0 12px; background: #161b22; border-bottom: 1px solid #30363d; scrollbar-width: none; flex-shrink: 0; } .tmdb-tabs::-webkit-scrollbar { display: none; } .tmdb-tab { padding: 10px 14px; font-size: 13px; color: #8b949e; cursor: pointer; white-space: nowrap; border-bottom: 2px solid transparent; transition: all .15s; user-select: none; } .tmdb-tab:hover { color: #e6edf3; background: rgba(88,166,255,.06); } .tmdb-tab.active { color: #58a6ff; border-bottom-color: #58a6ff; font-weight: 600; } /* 筛选栏 */ .tmdb-filter-bar { display: flex; align-items: center; gap: 8px; padding: 10px 20px; background: #0d1117; border-bottom: 1px solid #21262d; flex-shrink: 0; overflow-x: auto; scrollbar-width: none; } .tmdb-filter-bar::-webkit-scrollbar { display: none; } .tmdb-filter-label { font-size: 12px; color: #8b949e; white-space: nowrap; flex-shrink: 0; } .tmdb-chip { padding: 4px 12px; font-size: 12px; border-radius: 16px; border: 1px solid #30363d; background: #161b22; color: #8b949e; cursor: pointer; white-space: nowrap; transition: all .15s; flex-shrink: 0; } .tmdb-chip:hover { border-color: #58a6ff; color: #e6edf3; } .tmdb-chip.active { background: #1f6feb; border-color: #1f6feb; color: #fff; font-weight: 600; } /* 年份下拉 */ .tmdb-year-select { padding: 5px 10px; background: #161b22; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; font-size: 12px; outline: none; cursor: pointer; flex-shrink: 0; } .tmdb-year-select:focus { border-color: #58a6ff; } /* 内容区 */ .tmdb-content { flex: 1; overflow-y: auto; padding: 16px 20px; scrollbar-width: thin; scrollbar-color: #30363d transparent; } .tmdb-content::-webkit-scrollbar { width: 6px; } .tmdb-content::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; } .tmdb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; } .tmdb-card { background: #161b22; border-radius: 10px; overflow: hidden; border: 1px solid #21262d; transition: transform .2s, box-shadow .2s; cursor: pointer; position: relative; } .tmdb-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,.4); border-color: #30363d; } .tmdb-card img { width: 100%; aspect-ratio: 2/3; object-fit: cover; background: #21262d; display: block; } .tmdb-card-info { padding: 10px 12px; } .tmdb-card-title { font-size: 13px; font-weight: 600; line-height: 1.4; color: #e6edf3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 6px; } .tmdb-card-meta { font-size: 11px; color: #8b949e; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .tmdb-rating { display: inline-flex; align-items: center; gap: 3px; background: rgba(255,193,7,.12); color: #f0b429; padding: 2px 6px; border-radius: 4px; font-weight: 700; font-size: 12px; } .tmdb-rank { position: absolute; top: 8px; left: 8px; background: rgba(0,0,0,.75); color: #58a6ff; font-weight: 800; font-size: 14px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 6px; backdrop-filter: blur(4px); } /* 详情弹窗 */ .tmdb-detail-overlay { position: fixed; inset: 0; z-index: 2147483647; background: rgba(0,0,0,.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity .25s; color: #e6edf3; font-family: -apple-system, 'Segoe UI', Roboto, 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 14px; line-height: 1.6; } .tmdb-detail-overlay.show { opacity: 1; pointer-events: auto; } .tmdb-detail { width: 680px; max-height: 85vh; background: #161b22; border-radius: 14px; overflow: hidden; border: 1px solid #30363d; display: flex; flex-direction: column; color: #e6edf3; } .tmdb-detail-banner { position: relative; height: 260px; overflow: hidden; } .tmdb-detail-banner img { width: 100%; height: 100%; object-fit: cover; } .tmdb-detail-banner .tmdb-detail-gradient { position: absolute; inset: 0; background: linear-gradient(to top, #161b22 0%, transparent 60%); } .tmdb-detail-banner .tmdb-detail-close { position: absolute; top: 12px; right: 12px; background: rgba(0,0,0,.6); border: none; color: #fff; width: 32px; height: 32px; border-radius: 50%; font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center; } .tmdb-detail-body { padding: 20px; overflow-y: auto; flex: 1; color: #e6edf3; } .tmdb-detail-body h3 { margin: 0 0 8px; font-size: 20px; color: #e6edf3; } .tmdb-detail-meta { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 14px; font-size: 13px; color: #8b949e; } .tmdb-detail-overview { font-size: 14px; line-height: 1.7; color: #c9d1d9; margin-bottom: 16px; } .tmdb-detail-extra { font-size: 13px; color: #8b949e; } .tmdb-detail-extra p { color: #8b949e; } .tmdb-detail-extra span { color: #e6edf3; } .tmdb-loading { text-align: center; padding: 60px 0; color: #8b949e; } .tmdb-loading .spinner { width: 32px; height: 32px; border: 3px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: tmdb-spin .8s linear infinite; margin: 0 auto 12px; } @keyframes tmdb-spin { to { transform: rotate(360deg); } } .tmdb-empty { text-align: center; padding: 60px 0; color: #8b949e; font-size: 14px; } .tmdb-error { text-align: center; padding: 40px 20px; } .tmdb-error-icon { font-size: 48px; margin-bottom: 16px; } .tmdb-error-title { font-size: 16px; color: #f85149; margin-bottom: 8px; } .tmdb-error-msg { font-size: 13px; color: #8b949e; line-height: 1.6; } .tmdb-error-actions { margin-top: 20px; display: flex; gap: 10px; justify-content: center; } .tmdb-pagination { display: flex; justify-content: center; gap: 8px; padding: 16px 0 8px; flex-shrink: 0; } .tmdb-pagination button { padding: 6px 14px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e6edf3; cursor: pointer; font-size: 13px; } .tmdb-pagination button:hover { background: #30363d; } .tmdb-pagination button:disabled { opacity: .4; cursor: not-allowed; } .tmdb-page-info { color: #8b949e; font-size: 13px; line-height: 32px; } `); // ═══════════════════════════════════════════════════ // DOM // ═══════════════════════════════════════════════════ const root = document.createElement('div'); root.id = 'tmdb-panel-root'; document.body.appendChild(root); const fab = document.createElement('button'); fab.id = 'tmdb-fab'; fab.textContent = 'TMDB'; document.body.appendChild(fab); function renderShell() { root.innerHTML = `

📺 热播剧追剧面板

`; // 渲染 tabs const tabsEl = root.querySelector('#tmdb-tabs'); CONFIG.CATEGORIES.forEach(cat => { const tab = document.createElement('div'); tab.className = 'tmdb-tab' + (cat.id === currentTab ? ' active' : ''); tab.textContent = cat.label; tab.dataset.id = cat.id; tab.addEventListener('click', () => switchTab(cat.id)); tabsEl.appendChild(tab); }); // 事件 root.querySelector('#tmdb-close-btn').addEventListener('click', closePanel); root.querySelector('#tmdb-api-save').addEventListener('click', saveApiKey); root.querySelector('#tmdb-api-input').addEventListener('keydown', e => { if (e.key === 'Enter') saveApiKey(); }); root.querySelector('#tmdb-api-input').addEventListener('focus', function() { this.type = 'text'; }); root.querySelector('#tmdb-api-input').addEventListener('blur', function() { this.type = 'password'; }); root.querySelector('#tmdb-api-test').addEventListener('click', testConnection); root.querySelector('#tmdb-settings-btn').addEventListener('click', toggleSettings); root.querySelector('#tmdb-proxy-save').addEventListener('click', saveProxy); root.querySelector('#tmdb-proxy-clear').addEventListener('click', clearProxy); // 搜索事件 const searchInput = root.querySelector('#tmdb-search-input'); const searchClear = root.querySelector('#tmdb-search-clear'); searchInput.addEventListener('input', () => { const q = searchInput.value.trim(); searchClear.classList.toggle('show', q.length > 0); clearTimeout(searchTimer); if (q) { searchTimer = setTimeout(() => { searchQuery = q; currentPage = 1; doSearch(); }, 400); } else { searchQuery = ''; loadCurrentTab(); } }); searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') { clearTimeout(searchTimer); searchQuery = searchInput.value.trim(); currentPage = 1; if (searchQuery) doSearch(); else loadCurrentTab(); } }); searchClear.addEventListener('click', () => { searchInput.value = ''; searchQuery = ''; searchClear.classList.remove('show'); loadCurrentTab(); }); renderFilterBar(); } // ═══════════════════════════════════════════════════ // 筛选栏 // ═══════════════════════════════════════════════════ function renderFilterBar() { const bar = root.querySelector('#tmdb-filter-bar'); const cat = CONFIG.CATEGORIES.find(c => c.id === currentTab); if (searchQuery) { bar.innerHTML = ''; bar.style.display = 'none'; return; } bar.style.display = 'flex'; if (cat.filter === 'region') { // 地区筛选 chips bar.innerHTML = `🌏 地区:` + CONFIG.REGIONS.map(c => `${c.label}` ).join(''); bar.querySelectorAll('.tmdb-chip').forEach(chip => { chip.addEventListener('click', () => { filterRegion = chip.dataset.code; currentPage = 1; cachedData = {}; renderFilterBar(); loadCurrentTab(); }); }); } else if (cat.filter === 'year') { // 年份下拉 bar.innerHTML = `📅 年份:` + ``; bar.querySelector('#tmdb-year-select').addEventListener('change', e => { filterYear = e.target.value; currentPage = 1; cachedData = {}; loadCurrentTab(); }); } else { bar.innerHTML = ''; bar.style.display = 'none'; } } function switchTab(id) { currentTab = id; currentPage = 1; // 切换 tab 时重置筛选 filterRegion = ''; filterYear = ''; searchQuery = ''; const searchInput = root.querySelector('#tmdb-search-input'); if (searchInput) { searchInput.value = ''; } const searchClear = root.querySelector('#tmdb-search-clear'); if (searchClear) searchClear.classList.remove('show'); root.querySelectorAll('.tmdb-tab').forEach(t => t.classList.toggle('active', t.dataset.id === id)); renderFilterBar(); loadCurrentTab(); } // ═══════════════════════════════════════════════════ // 加载数据 // ═══════════════════════════════════════════════════ async function loadCurrentTab() { const content = root.querySelector('#tmdb-content'); const pagination = root.querySelector('#tmdb-pagination'); if (!CONFIG.API_KEY) { content.innerHTML = '
🔑 请先在上方输入 TMDB API Key 并点击保存
'; pagination.innerHTML = ''; return; } content.innerHTML = '
加载中…
'; pagination.innerHTML = ''; const cat = CONFIG.CATEGORIES.find(c => c.id === currentTab); const cacheKey = `${currentTab}_p${currentPage}_r${filterRegion}_y${filterYear}`; try { let items; if (cachedData[cacheKey]) { items = cachedData[cacheKey]; } else { items = await fetchCategory(cat, currentPage, { country: filterRegion, year: filterYear }); cachedData[cacheKey] = items; } renderCards(items); renderPagination(items.length); } catch (err) { log('加载失败:', err); renderError(content, err.message); pagination.innerHTML = ''; } } async function doSearch() { const content = root.querySelector('#tmdb-content'); const pagination = root.querySelector('#tmdb-pagination'); const bar = root.querySelector('#tmdb-filter-bar'); if (!CONFIG.API_KEY) { content.innerHTML = '
🔑 请先输入 API Key
'; return; } bar.style.display = 'none'; content.innerHTML = '
搜索中…
'; pagination.innerHTML = ''; try { const items = await searchTV(searchQuery, currentPage); renderCards(items); renderPagination(items.length, true); } catch (err) { renderError(content, err.message); } } function renderError(container, msg) { container.innerHTML = `
🌐
加载失败
${msg}
`; container.querySelector('#tmdb-retry').addEventListener('click', () => { cachedData = {}; workingHost = ''; GM_setValue('tmdb_working_host', ''); if (searchQuery) doSearch(); else loadCurrentTab(); }); container.querySelector('#tmdb-open-settings').addEventListener('click', () => { showSettings = true; root.querySelector('#tmdb-settings').style.display = 'block'; }); } function renderCards(items) { const content = root.querySelector('#tmdb-content'); if (!items.length) { content.innerHTML = '
📭 暂无数据
'; return; } const grid = document.createElement('div'); grid.className = 'tmdb-grid'; items.forEach((item, i) => { const card = document.createElement('div'); card.className = 'tmdb-card'; const poster = item.poster_path ? `${CONFIG.IMG_BASE}w342${item.poster_path}` : ''; const rating = item.vote_average ? item.vote_average.toFixed(1) : 'N/A'; const year = (item.first_air_date || '').slice(0, 4); const rank = (currentPage - 1) * CONFIG.PAGE_SIZE + i + 1; card.innerHTML = ` ${rank <= 20 ? `
${rank}
` : ''} ${poster ? `${item.name||''}` : ''}
🎬
${item.name || item.original_name || '未知'}
⭐ ${rating} ${year ? `${year}` : ''} ${item.origin_country ? `${item.origin_country.join('/')}` : ''}
`; card.addEventListener('click', () => showDetail(item.id)); grid.appendChild(card); }); content.innerHTML = ''; content.appendChild(grid); } function renderPagination(resultCount, isSearch = false) { const pagination = root.querySelector('#tmdb-pagination'); if (currentPage === 1 && resultCount < CONFIG.PAGE_SIZE) { pagination.innerHTML = ''; return; } pagination.innerHTML = ` 第 ${currentPage} 页 `; pagination.querySelector('#tmdb-prev').addEventListener('click', () => { if (currentPage > 1) { currentPage--; if (isSearch) doSearch(); else loadCurrentTab(); } }); pagination.querySelector('#tmdb-next').addEventListener('click', () => { if (resultCount >= CONFIG.PAGE_SIZE) { currentPage++; if (isSearch) doSearch(); else loadCurrentTab(); } }); } // ═══════════════════════════════════════════════════ // 详情弹窗 // ═══════════════════════════════════════════════════ async function showDetail(tvId) { const old = document.querySelector('.tmdb-detail-overlay'); if (old) old.remove(); const overlay = document.createElement('div'); overlay.className = 'tmdb-detail-overlay'; overlay.innerHTML = '
加载详情…
'; document.body.appendChild(overlay); requestAnimationFrame(() => overlay.classList.add('show')); overlay.addEventListener('click', e => { if (e.target === overlay) closeOverlay(overlay); }); try { const d = await fetchTVDetail(tvId); const backdrop = d.backdrop_path ? `${CONFIG.IMG_BASE}w780${d.backdrop_path}` : ''; const genres = (d.genres||[]).map(g=>g.name).join(' / '); const networks = (d.networks||[]).map(n=>n.name).join(', '); const rating = d.vote_average ? d.vote_average.toFixed(1) : 'N/A'; const pad = n => String(n).padStart(2,'0'); const nextEp = d.next_episode_to_air; const nextEpStr = nextEp ? `S${pad(nextEp.season_number)}E${pad(nextEp.episode_number)} (${nextEp.air_date})` : '暂无'; const lastEp = d.last_episode_to_air; const lastEpStr = lastEp ? `S${pad(lastEp.season_number)}E${pad(lastEp.episode_number)} (${lastEp.air_date}) — ${lastEp.name||''}` : ''; overlay.querySelector('.tmdb-detail').innerHTML = `
${backdrop ? `` : ''}

${d.name||d.original_name||'未知'}

⭐ ${rating} ${genres ? `🎭 ${genres}` : ''} 📅 ${d.first_air_date||'-'}
${d.overview||'暂无简介'}

📺 状态:${d.status||'-'} │ 季数:${d.number_of_seasons||'-'} │ 集数:${d.number_of_episodes||'-'}

🏢 平台:${networks||'-'}

📺 下一集:${nextEpStr}

${lastEpStr ? `

⏪ 最近更新:${lastEpStr}

` : ''}

🌍 原产国:${(d.origin_country||[]).join(', ')||'-'}

🗣️ 原语言:${d.original_language||'-'}

`; overlay.querySelector('.tmdb-detail-close').addEventListener('click', () => closeOverlay(overlay)); } catch (err) { overlay.querySelector('.tmdb-detail').innerHTML = `
❌ ${err.message}
`; } } function closeOverlay(overlay) { overlay.classList.remove('show'); setTimeout(() => overlay.remove(), 250); } // ═══════════════════════════════════════════════════ // 面板开关 & 辅助 // ═══════════════════════════════════════════════════ function openPanel() { root.classList.add('open'); } function closePanel() { root.classList.remove('open'); } fab.addEventListener('click', () => root.classList.toggle('open')); document.addEventListener('keydown', e => { if (e.key === 'Escape') { const overlay = document.querySelector('.tmdb-detail-overlay.show'); if (overlay) closeOverlay(overlay); else closePanel(); } }); function setStatus(el, text, color='#8b949e') { const s = root.querySelector(el); if (s) { s.textContent = text; s.style.color = color; } setTimeout(() => { if (s) s.textContent = ''; }, 3000); } function saveApiKey() { const key = root.querySelector('#tmdb-api-input').value.trim(); if (!key) return; CONFIG.API_KEY = key; GM_setValue('tmdb_api_key', key); cachedData = {}; workingHost = ''; GM_setValue('tmdb_working_host', ''); setStatus('#tmdb-api-status', '✓ 已保存', '#3fb950'); loadCurrentTab(); } async function testConnection() { setStatus('#tmdb-api-status', '⏳ 测试中…', '#58a6ff'); try { await tryFetch('/configuration'); setStatus('#tmdb-api-status', '✓ 连接成功!', '#3fb950'); } catch (err) { setStatus('#tmdb-api-status', '✗ '+err.message, '#f85149'); } } function toggleSettings() { showSettings = !showSettings; root.querySelector('#tmdb-settings').style.display = showSettings ? 'block' : 'none'; } function saveProxy() { const host = root.querySelector('#tmdb-proxy-input').value.trim().replace(/\/+$/,''); CONFIG.PROXY_HOST = host; GM_setValue('tmdb_proxy_host', host); if (host) { workingHost = host; GM_setValue('tmdb_working_host', host); } cachedData = {}; setStatus('#tmdb-proxy-status', '✓ 已保存', '#3fb950'); loadCurrentTab(); } function clearProxy() { CONFIG.PROXY_HOST = ''; GM_setValue('tmdb_proxy_host', ''); root.querySelector('#tmdb-proxy-input').value = ''; cachedData = {}; setStatus('#tmdb-proxy-status', '✓ 已清除', '#3fb950'); } // ═══════════════════════════════════════════════════ // 初始化 // ═══════════════════════════════════════════════════ (function init() { const saved = GM_getValue('tmdb_api_key', ''); if (saved) CONFIG.API_KEY = saved; const savedProxy = GM_getValue('tmdb_proxy_host', ''); if (savedProxy) CONFIG.PROXY_HOST = savedProxy; const savedHost = GM_getValue('tmdb_working_host', ''); if (savedHost) workingHost = savedHost; renderShell(); loadCurrentTab(); GM_registerMenuCommand('打开热播剧面板', openPanel); log('面板已初始化'); })(); })();