// ==UserScript==
// @name 腾果影视
// @namespace https://scriptcat.org/zh-CN/
// @version 1.1.2
// @description 爱看纯净版:爱看1/爱看2 双站聚合搜索,m3u8 直链解析,多线路切换 + 实时测速,自动连播,统一观看历史,红果详情页一键搜索,腾讯片库海报一键搜爱看(弹窗播放)。仅限个人学习使用。https://v.qq.com/tv-series-list/v-index.html
// @author 失辛向南
// @license MIT
// @run-at document-start
// @noframes
// @match *://www.imoviebot.com/*
// @match *://hongguoduanju.com/*
// @match *://*.hongguoduanju.com/*
// @match *://ikanbot.eu.org/*
// @match *://*.ikanbot.eu.org/*
// @match *://www1.ikanbot.com/*
// @match *://*.www1.ikanbot.com/*
// @match *://v.qq.com/tv-series-list/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_info
// @grant unsafeWindow
// @connect www.hongniuzy2.com
// @connect bfzyapi.com
// @connect cj.ffzyapi.com
// @connect cj.lziapi.com
// @connect ikunzyapi.com
// @connect api.guangsuapi.com
// @connect suoniapi.com
// @connect sdzyapi.com
// @connect collect.wolongzyw.com
// @connect api.apibdzy.com
// @connect cj.yayazy.net
// @connect jyzyapi.com
// @connect api.wujinapi.me
// @connect www.huyaapi.com
// @connect caiji.dyttzyapi.com
// @connect p2100.net
// @connect zuidazy.me
// @connect api.zuidapi.com
// @connect caiji.moduapi.cc
// @connect www.mdzyapi.com
// @connect wolongzyw.com
// @connect api.xinlangapi.com
// @connect iqiyizyapi.com
// @connect caiji.dbzy5.com
// @connect tyyszy.com
// @connect cj.rycjapi.com
// @connect jinyingzy.com
// @connect jszyapi.com
// @connect wjzyapi.com
// @connect apiyutu.com
// @connect hongguoduanju.com
// @connect *.hongguoduanju.com
// @connect ikanbot.eu.org
// @connect *.ikanbot.eu.org
// @connect www1.ikanbot.com
// @connect *.www1.ikanbot.com
// @require https://cdnjs.cloudflare.com/ajax/libs/artplayer/5.1.0/artplayer.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.9/vue.min.js
// ==/UserScript==
(function () {
'use strict';
// ==================== 站点识别 ====================
const HOSTNAME = location.hostname;
const PATHNAME = location.pathname;
const HREF = location.href;
const isDuanjuDomain = HOSTNAME.indexOf('hongguoduanju') !== -1;
const isImoviebotDomain = HOSTNAME.indexOf('ikanbot') !== -1 || HOSTNAME.indexOf('imoviebot') !== -1;
// 腾讯片库:仅 v.qq.com 的 tv-series-list 频道列表页启用频道整合卡片墙
const isQQDomain = HOSTNAME === 'v.qq.com';
const isTencentList = isQQDomain && PATHNAME.indexOf('/tv-series-list/') !== -1;
// 站点代号:dj=红果短剧, im=爱看影视, tx=腾讯片库;其他页面兜底为 db(无专门适配)
const CURRENT_SITE = isTencentList ? 'tx'
: (isDuanjuDomain ? 'dj'
: (isImoviebotDomain ? 'im' : 'db'));
// 统一布局判断:宽度≥769 或横屏 → PC 版;否则移动版
const useDesktopLayout = () => window.innerWidth >= 769 || window.innerWidth > window.innerHeight;
// 红果短剧广告屏蔽
if (isDuanjuDomain) {
const adCss = '.mobile-guide-container-Cz30NR,.m-container-HjIuqt,.m-container-GjpJgU,.rp-hanger-EfEun1,.pc-hero-btn-download-CHc_Va,.pc-rp-wrap-lhRmt9,.pc-episode-item-7KBv6v,.pc-episode-toggle-AX4DVV{display:none!important;}';
const addStyle = () => {
const s = document.createElement('style');
s.textContent = adCss;
(document.head || document.documentElement).appendChild(s);
};
if (document.head) addStyle();
else document.addEventListener('DOMContentLoaded', addStyle);
}
// 跨脚本通信契约:找不到"加载更多"按钮的重试计数(供播放层判断)
try { window._noButtonRetryCount = 0; } catch (e) {}
// ==================== 诊断日志系统 ====================
const __diag = (() => {
const MAX = 800;
const lines = [];
const scriptName = '豆果影视';
// 版本号单一来源:优先取 GM 元数据中的实际安装版本,避免头注版本与内部版本不一致(ISS-001)
const version = (() => {
try {
if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) {
return String(GM_info.script.version);
}
} catch (e) {}
return '7.4.18';
})();
const nowStr = () => {
try { return new Date().toLocaleString('zh-CN', { hour12: false }); } catch (e) { return new Date().toString(); }
};
const safeStr = (o) => {
try {
if (o instanceof Error) return o.name + ': ' + o.message + (o.stack ? '\n' + o.stack.split('\n').slice(0, 6).join('\n') : '');
if (typeof o === 'string') return o;
return JSON.stringify(o);
} catch (e) { return String(o); }
};
const push = (level, msg) => {
try {
let s = safeStr(msg);
if (s.length > 600) s = s.slice(0, 600) + '…';
lines.push('[' + nowStr() + '][' + level + '] ' + s);
if (lines.length > MAX) lines.shift();
} catch (e) {}
};
const wrapConsole = () => {
['log', 'info', 'warn', 'error', 'debug'].forEach((m) => {
const orig = console[m];
if (typeof orig !== 'function') return;
try {
console[m] = function () {
try {
const args = Array.prototype.slice.call(arguments);
if (m === 'error' || m === 'warn') {
push(m.toUpperCase(), args.map((a) => typeof a === 'string' ? a : safeStr(a)).join(' '));
} else {
// log/info 仅收集以 '[' 开头的结构化日志:先预判首个参数,避免对无关调用做 join 处理
const first = args[0];
if (typeof first === 'string' && first.charAt(0) === '[') {
push(m.toUpperCase(), args.map((a) => typeof a === 'string' ? a : safeStr(a)).join(' '));
}
}
} catch (e) {}
return orig.apply(console, arguments);
};
} catch (e) {}
});
};
const hookErrors = () => {
window.addEventListener('error', (ev) => {
try {
// 资源加载失败(img/script/link 等):单独记录具体资源 URL
const t = ev.target;
if (t && t.tagName && !(ev.message)) {
const src = t.src || t.currentSrc || t.href || '';
push('ERROR', '资源加载失败: <' + String(t.tagName).toLowerCase() + '> ' + (src || '(无URL)'));
return;
}
let msg = ev.message || '';
const loc = ((ev.filename || '') + ':' + (ev.lineno || '') + ':' + (ev.colno || '')).replace(/^::/, '');
if (ev.error && ev.error.message) msg = ev.error.message;
// 过滤无 message、无位置、无 error 对象的空错误事件(多为扩展/无关资源注入,无排障价值)
if (!msg && !loc && !ev.error) return;
let extra = '';
if (ev.error && ev.error.stack) {
extra = '\n' + ev.error.stack.split('\n').slice(0, 5).join('\n');
}
push('ERROR', 'window.onerror: ' + msg + ' @ ' + loc + extra);
} catch (e2) {}
}, true);
window.addEventListener('unhandledrejection', (ev) => {
const r = ev.reason;
push('ERROR', 'unhandledrejection: ' + (r && r.message ? r.message : safeStr(r)));
});
};
const buildReport = () => {
const out = [];
out.push('========== ' + scriptName + ' 诊断日志 ==========');
out.push('版本: ' + version);
out.push('生成时间: ' + nowStr());
out.push('站点: ' + CURRENT_SITE);
out.push('页面: ' + location.href);
out.push('UA: ' + navigator.userAgent);
out.push('视口: ' + (window.innerWidth || '') + 'x' + (window.innerHeight || ''));
try {
out.push('自动连播: ' + GM_getValue(AUTO_NEXT_ENABLED_KEY, true));
out.push('主题: ' + getTheme());
const allKeys = GM_listValues();
const stats = {};
allKeys.forEach((k) => {
const prefix = k.split('_')[0];
stats[prefix] = (stats[prefix] || 0) + 1;
});
out.push('GM存储键统计: ' + safeStr(stats) + ' (共' + allKeys.length + '个键)');
} catch (e) { out.push('存储状态读取失败: ' + safeStr(e)); }
out.push('-------- 日志记录 (' + lines.length + ' 条) --------');
for (let i = 0; i < lines.length; i++) out.push(lines[i]);
return out.join('\n');
};
const download = () => {
try {
const text = buildReport();
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const d = new Date(), pad = (n) => n < 10 ? '0' + n : '' + n;
a.download = scriptName + '-诊断日志-' + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + '.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => { try { URL.revokeObjectURL(url); } catch (e2) {} }, 5000);
return true;
} catch (e) {
try { prompt('无法自动下载日志,请复制以下内容:', buildReport()); } catch (e2) {}
return false;
}
};
wrapConsole();
hookErrors();
return { push, download, buildReport, info: (m) => push('INFO', m), warn: (m) => push('WARN', m), error: (m) => push('ERROR', m) };
})();
__diag.info('脚本已加载,版本 ' + __diag.version);
// ==================== 阶段一:document-start(红果根跳转)====================
let didRedirect = false;
// 红果短剧主域名直接跳转到分类页
if (isDuanjuDomain && PATHNAME === '/' && !location.search) {
location.replace('https://hongguoduanju.com/category?sort_type=1');
didRedirect = true;
}
// 重定向已触发则当前页面即将跳转,不再注入任何内容
if (didRedirect) return;
// ==================== Artplayer 工具 ====================
// ISS-003:外部库缺失守卫。@require CDN 失败/被替换时不再整脚本崩溃:
// $ 退化为标准 querySelector、isMobile 退化为 UA 嗅探;Artplayer 正常时解构结果与原来完全一致。
const ArtplayerReady = !!(typeof Artplayer !== 'undefined' && Artplayer && Artplayer.utils);
const { query: $ = (sel, root) => (root || document).querySelector(sel),
isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) } = (ArtplayerReady ? Artplayer.utils : {});
// ==================== 配置区 ====================
const ANTI_BLOCK_CONFIG = {
maxConcurrent: 3,
requestJitterMin: 100,
requestJitterMax: 300,
};
// 搜索早停(默认关):命中 ≥EARLY_EXIT_MIN_SUCCESS 个成功源后,未开始的源不再发请求。
// 默认关闭以保持搜索结果集与原版完全一致;如需省请求可置为 true。
const SEARCH_EARLY_EXIT_ENABLED = false;
const EARLY_EXIT_MIN_SUCCESS = 3;
const CACHE_TTL = 6 * 60 * 60 * 1000; // 成功搜索缓存 6 小时
const SEARCH_FAILURE_CACHE_TTL = 10 * 60 * 1000; // 失败搜索只短缓存 10 分钟:瞬时故障不长时间锁死重试
const MAX_CACHE_ITEMS_PER_MOVIE = 50;
const MAX_HISTORY_ITEMS = 80;
// 测速结果会话缓存:同页面生命周期内 10 分钟内复用,避免重复下载 ts 分片测速
const SPEED_TEST_CACHE_TTL = 10 * 60 * 1000;
const speedTestSessionCache = new Map(); // sourceName -> { speed, time }
const AUTO_NEXT_ENABLED_KEY = 'autoPlayNextEnabled';
const UNIFIED_HISTORY_KEY = 'unified_watch_history';
// 红果详情页开关:true=点海报进详情页,由详情页"一键搜索"触发搜索;false(默认)=点海报直接弹窗搜索片名
const HONGUO_DETAIL_KEY = 'hgDetailPageEnabled';
const PROGRESS_PREFIX = 'play_'; // play_{site}_{id}
const TIP_SHOWN_PREFIX = 'tip_shown_'; // tip_shown_{uid},手机端一次性提示标记
const MIGRATION_KEY = 'v7_migration_done';
const SEARCH_HISTORY_KEY = 'search_keyword_history';
const THEME_KEY = 'app_theme'; // 'dark' | 'light'
const MAX_SEARCH_HISTORY = 12;
// 搜索关键词历史
const getSearchHistory = () => {
try {
const list = GM_getValue(SEARCH_HISTORY_KEY, []) || [];
console.log('[阿豆影视] 读取搜索历史:', list.length, '条');
return list;
} catch (e) { console.warn('[阿豆影视] 读取搜索历史失败:', e); return []; }
};
const addSearchHistory = (keyword) => {
if (!keyword) return;
let list = getSearchHistory();
list = list.filter(k => k !== keyword);
list.unshift(keyword);
list = list.slice(0, MAX_SEARCH_HISTORY);
try {
GM_setValue(SEARCH_HISTORY_KEY, list);
console.log('[阿豆影视] 保存搜索历史:', keyword, '共', list.length, '条');
} catch (e) { console.warn('[阿豆影视] 保存搜索历史失败:', e); }
};
const clearSearchHistory = () => GM_setValue(SEARCH_HISTORY_KEY, []);
// 清空搜索相关数据(历史关键词),弹窗"清除"共用
const clearSearchRelatedData = () => {
let count = 0;
if (GM_getValue(SEARCH_HISTORY_KEY, undefined) !== undefined) { GM_deleteValue(SEARCH_HISTORY_KEY); count++; }
return count;
};
// 主题管理
const getTheme = () => GM_getValue(THEME_KEY, 'light');
const setTheme = (theme) => {
GM_setValue(THEME_KEY, theme);
document.documentElement.setAttribute('data-adys-theme', theme);
};
const initTheme = () => {
document.documentElement.setAttribute('data-adys-theme', getTheme());
};
// 尽早初始化主题,避免闪烁
initTheme();
// ==================== 影片数据提取(站点感知)====================
let videoDataBase = null;
function getVideoDataOnce() {
if (videoDataBase) return videoDataBase;
const data = {
videoName: '未知',
videoYear: '',
videoFirstActor: '',
posterUrl: '',
site: CURRENT_SITE,
id: 'unknown_' + Date.now(),
subjectUrl: HREF,
};
try {
if (CURRENT_SITE === 'dj') {
// 红果有两种详情页 URL 格式,都需要提取 series_id:
// 1. /detail?series_id=7650505082215091224 → 直接从查询参数提取
// 2. /series/toushizhiyan1133208616 → 从路径末尾的数字提取
// 3. /player/7678593905394191422 → 从路径提取(新版详情/播放页)
const currentHref = location.href;
const m = currentHref.match(/series_id=(\d+)/);
if (m) {
data.id = m[1];
} else {
const m2 = currentHref.match(/\/series\/[^/]*?(\d+)(?:[/?#]|$)/);
if (m2) {
data.id = m2[1];
} else {
const m3 = currentHref.match(/\/player\/(\d+)/);
if (m3) data.id = m3[1];
}
}
data.subjectUrl = currentHref;
// 红果短剧标题提取:页面标题格式 "红果短剧 | XXX全集免费观看"
const titleMatch = document.title.match(/^红果短剧\s*\|\s*(.*?)全集免费观看/);
if (titleMatch && titleMatch[1]) data.videoName = titleMatch[1].trim();
else {
// 移动端标题可能不同,尝试更多匹配模式
const titleMatch2 = document.title.match(/红果短剧\s*[-|]\s*(.+)/);
const h1 = document.querySelector('h1, [class*="drama-title"], [class*="series-name"], [class*="title-text"]');
if (h1) {
// 排除脚本自身注入的按钮文本(一键搜索/搜索中…等),避免标题污染搜索关键词与历史记录
const clone = h1.cloneNode(true);
clone.querySelectorAll('[class*="adys-"], .liu-btn, .play-btn').forEach(n => n.remove());
data.videoName = (clone.innerText || h1.innerText).trim();
}
else if (titleMatch2 && titleMatch2[1]) data.videoName = titleMatch2[1].replace(/全集免费观看$/, '').trim();
else data.videoName = document.title.replace(/^红果短剧\s*[-|]\s*/, '').replace(/全集免费观看$/, '').trim() || document.title.trim();
}
// 红果短剧海报提取:优先从 _ROUTER_DATA SSR 数据获取(最可靠)
// 策略0:从 window._ROUTER_DATA.loaderData 中查找 seriesDetail
//(新版页面 loaderData 键为 player_(series_id)/page 等动态键,需遍历查找)
try {
const rd = window._ROUTER_DATA;
if (rd && rd.loaderData) {
let sd = null;
if (rd.loaderData.detail_page && rd.loaderData.detail_page.seriesDetail) {
sd = rd.loaderData.detail_page.seriesDetail;
} else {
for (const k in rd.loaderData) {
const v = rd.loaderData[k];
if (v && typeof v === 'object' && v.seriesDetail) { sd = v.seriesDetail; break; }
}
}
if (sd) {
if (sd.series_cover) data.posterUrl = sd.series_cover;
if (sd.series_name && data.videoName === '未知') data.videoName = sd.series_name;
}
}
} catch(e) {}
// 策略1:精确匹配主封面容器内的 img(排除 logo、头像、推荐列表封面)
if (!data.posterUrl) {
const mainPosterImg = document.querySelector('.pc-img-y1TjVL img, [class*="img-box"] > [class*="img-vB9HMs"] img, [class*="m-img"] img[src*="byteimg"], [class*="cover"] > img[src*="byteimg"]');
if (mainPosterImg && mainPosterImg.src) {
data.posterUrl = mainPosterImg.src;
}
}
// 策略2:匹配来自 byteimg.com 的大尺寸封面图(width >= 600),排除头像(200px)和 logo
if (!data.posterUrl) {
const byteImgs = document.querySelectorAll('img[src*="novel.byteimg.com"]');
let firstBigImg = '';
for (const img of byteImgs) {
if (!img.src || !(img.naturalWidth >= 600 || img.width >= 600)) continue;
// 记录第一张大图作兜底
if (!firstBigImg) firstBigImg = img.src;
// 排除推荐列表封面(parentCls 包含 pc-img-Lpi5Zs)
const parentCls = img.parentElement ? img.parentElement.className : '';
if (parentCls.indexOf('pc-img-Lpi5Zs') === -1 && parentCls.indexOf('m-img-MqFAvn') === -1) {
firstBigImg = img.src;
break;
}
}
data.posterUrl = firstBigImg;
}
// 策略3:回退到 og:image
if (!data.posterUrl) data.posterUrl = getOgImage();
}
videoDataBase = data;
return data;
} catch (e) {
tip(CURRENT_SITE === 'dj' ? '短剧数据读取失败' : '影片数据读取失败');
return null;
}
}
const getUid = () => {
const d = getVideoDataOnce();
return d ? `${d.site}_${d.id}` : 'unknown_' + Date.now();
};
// 播放进度按"片名"存取(而非 site_id),保证同一片名从红果详情/腾讯片库/搜索弹窗/历史
// 任意入口进入都续播同一集、同一源。
// 归一化:去噪声词 + 去标点空白,并单独提取结尾年份。
// 匹配规则:基础名一致才算同一部;年份都存在但不同(不同季/翻拍)→ 不合并,避免进度串台。
const titleParts = (s) => {
if (!s) return { base: '', year: '' };
let r = removeNoiseWords(s);
let year = '';
const ym = r.match(/[((\[【]?\s*((?:19|20)\d{2})\s*[))\]】]?\s*$/);
if (ym) { year = ym[1]; r = r.slice(0, ym.index).trim(); }
r = r.replace(/[\s\-—–::,,.。!!??'"“”‘’·、]/g, '');
return { base: r.toLowerCase(), year };
};
// 进度存储键:有年份带年份,无年份不带(带年份片名与腾讯"X"通过 matchTitle 桥接)
const titleKey = (s) => {
const { base, year } = titleParts(s);
return base + (year ? '|' + year : '');
};
// 同名片匹配:基础名一致,且年份不冲突(两侧都有年份但不同 → 视为不同季/翻拍,不合并)
const matchTitle = (a, b) => {
const pa = titleParts(a), pb = titleParts(b);
if (!pa.base || !pb.base || pa.base !== pb.base) return false;
if (pa.year && pb.year && pa.year !== pb.year) return false;
return true;
};
const getProgressKey = () => {
const d = getVideoDataOnce();
const name = d && d.videoName ? titleKey(d.videoName) : '';
if (name) return PROGRESS_PREFIX + 't_' + name;
return PROGRESS_PREFIX + getUid();
};
const savePlayHistory = (data) => {
try {
const key = getProgressKey();
const newData = { ...GM_getValue(key, {}), ...data, updateTime: Date.now() };
GM_setValue(key, newData);
updateUnifiedHistoryPlayData(newData);
} catch (e) {}
};
const getPlayHistory = () => {
try {
const local = GM_getValue(getProgressKey(), {});
// 跨入口:从统一历史条目按片名补回已存的进度(选集/选集源/时间点);
// matchTitle 容忍带年份/不带年份的片名,但拒绝不同年份的翻拍与季
const d = getVideoDataOnce();
if (d && d.videoName) {
const hist = getUnifiedHistory();
const hit = hist.find(it => it && matchTitle(it.movieName || '', d.videoName));
if (hit && hit.playHistory) return { ...hit.playHistory, ...local };
}
return local;
} catch (e) { return {}; }
};
// ==================== 统一历史管理 ====================
// 删除被挤出历史条目的孤儿键(播放进度 + 一次性提示标记),避免 GM 存储长期堆积
const pruneOrphanHistoryKeys = (dropped, keptIds) => {
if (!dropped || dropped.length === 0) return;
dropped.forEach(item => {
if (item && item.movieId && !keptIds.has(item.movieId)) {
GM_deleteValue(PROGRESS_PREFIX + item.movieId);
GM_deleteValue(TIP_SHOWN_PREFIX + item.movieId);
}
});
};
const getUnifiedHistory = () => {
try {
let history = GM_getValue(UNIFIED_HISTORY_KEY, []);
if (!Array.isArray(history)) history = [];
const trimmed = history.slice(0, MAX_HISTORY_ITEMS);
if (trimmed.length !== history.length) {
// 防御:旧版本残留的超限数据,读取时一并截断并清理孤儿键
const keptIds = new Set(trimmed.filter(item => item && item.movieId).map(item => item.movieId));
pruneOrphanHistoryKeys(history.slice(MAX_HISTORY_ITEMS), keptIds);
saveUnifiedHistory(trimmed);
}
return trimmed;
} catch (e) { return []; }
};
const saveUnifiedHistory = (history) => {
try {
const trimmed = history.slice(0, MAX_HISTORY_ITEMS);
GM_setValue(UNIFIED_HISTORY_KEY, trimmed);
// 历史被自动截断(超出 MAX_HISTORY_ITEMS)时,被挤出条目的播放进度键与
// 一次性提示标记一并物理删除,避免孤儿键在 GM 存储中长期堆积
if (trimmed.length !== history.length) {
const keptIds = new Set(trimmed.filter(item => item && item.movieId).map(item => item.movieId));
pruneOrphanHistoryKeys(history.slice(MAX_HISTORY_ITEMS), keptIds);
}
} catch (e) {}
};
const addToUnifiedHistory = (uid, name, subjectUrl, playData = {}, poster = '', sourceData = null) => {
if (!uid || !name) return;
let history = getUnifiedHistory();
// 同一影视去重:同 movieId 或同片名(matchTitle:基础名一致且年份不冲突)只保留一条,命中则更新到最前。
// 不同年份的翻拍/季(如 1997 版 vs 2003 版)不合并,避免历史与进度串台。
const idx = history.findIndex(item =>
item.movieId === uid || matchTitle(item.movieName || '', name)
);
// 优先使用传入的海报,否则从当前页面获取
let posterUrl = poster;
if (!posterUrl) {
const videoData = getVideoDataOnce();
posterUrl = (videoData && videoData.posterUrl) || '';
}
const entry = {
movieId: uid, movieName: name, subjectUrl, posterUrl, timestamp: Date.now(), playHistory: playData,
// 存播放源:历史点击时直接从源唤起播放器,不再重新搜索
sourceData: sourceData && sourceData.sources && sourceData.sources.length > 0
? { title: sourceData.title || name, poster: sourceData.poster || posterUrl, sources: sourceData.sources }
: null
};
if (idx !== -1) {
const old = history[idx];
// 合并:保留旧条目更完整的海报/链接,更新播放进度与时间戳;源优先补新;
// playHistory 合并而非整体替换,避免刚开播只写了 selectedSource 就抹掉旧的选集/时间点
history[idx] = { ...old, ...entry, posterUrl: old.posterUrl || posterUrl, playHistory: { ...old.playHistory, ...entry.playHistory }, timestamp: Date.now() };
if (!history[idx].sourceData) history[idx].sourceData = entry.sourceData;
} else history.unshift(entry);
history.sort((a, b) => b.timestamp - a.timestamp);
saveUnifiedHistory(history);
};
// 统一历史播放数据节流写入:进度小幅推进时跳过整数组读改写(每 30s 或跳变 ≥30s / duration 变化才写)
const lastUnifiedPlayWriteMap = new Map(); // 规范化片名 -> {time, currentTime, duration}
const updateUnifiedHistoryPlayData = (playData) => {
const now = Date.now();
const d = getVideoDataOnce();
const curName = d && d.videoName ? d.videoName : '';
const last = curName ? lastUnifiedPlayWriteMap.get(curName) : null;
if (last) {
const dT = (playData.currentTime || 0) - last.currentTime;
if (now - last.time < 30000 && Math.abs(dT) < 30 && playData.duration === last.duration) {
return; // 明细进度键仍由调用方周期写入,此处仅跳过全历史数组的读改写
}
}
if (curName) lastUnifiedPlayWriteMap.set(curName, { time: now, currentTime: playData.currentTime || 0, duration: playData.duration });
let history = getUnifiedHistory();
const idx = curName ? history.findIndex(item => item && matchTitle(item.movieName || '', curName)) : -1;
if (idx !== -1) {
history[idx].playHistory = { ...history[idx].playHistory, ...playData, updateTime: Date.now() };
history[idx].timestamp = Date.now();
saveUnifiedHistory(history);
}
};
const deleteUnifiedHistoryItem = (uid) => {
let history = getUnifiedHistory();
const entry = history.find(item => item.movieId == uid);
const newHistory = history.filter(item => item.movieId != uid);
if (newHistory.length === history.length) return false;
saveUnifiedHistory(newHistory);
GM_deleteValue(PROGRESS_PREFIX + uid);
GM_deleteValue(TIP_SHOWN_PREFIX + uid);
// 同时清理按片名存取的进度键与提示标记(带年份与不带年份两个可能键都清)
if (entry && entry.movieName) {
const parts = titleParts(entry.movieName);
GM_deleteValue(PROGRESS_PREFIX + 't_' + parts.base);
GM_deleteValue(TIP_SHOWN_PREFIX + 't_' + parts.base);
if (parts.year) {
GM_deleteValue(PROGRESS_PREFIX + 't_' + parts.base + '|' + parts.year);
GM_deleteValue(TIP_SHOWN_PREFIX + 't_' + parts.base + '|' + parts.year);
}
}
return true;
};
const clearAllUnifiedHistory = () => {
saveUnifiedHistory([]);
const allKeys = GM_listValues();
allKeys.forEach(key => {
if (key.startsWith(PROGRESS_PREFIX) || key.startsWith(TIP_SHOWN_PREFIX)) GM_deleteValue(key);
});
tip('所有观看记录已清除');
};
// ==================== 旧数据迁移(一次性)====================
function migrateLegacyData() {
if (GM_getValue(MIGRATION_KEY, false)) return;
try {
// 1) 统一历史:合并旧短剧历史 + 为旧条目补站点前缀
let merged = GM_getValue(UNIFIED_HISTORY_KEY, []);
if (!Array.isArray(merged)) merged = [];
const oldDj = GM_getValue('dj_unified_watch_history', []);
if (Array.isArray(oldDj) && oldDj.length) {
const exist = new Set(merged.map(i => i.movieId));
oldDj.forEach(item => {
if (item.movieId) item.movieId = 'dj_' + item.movieId;
if (item.movieId && !exist.has(item.movieId)) merged.push(item);
});
merged.sort((a, b) => b.timestamp - a.timestamp);
GM_deleteValue('dj_unified_watch_history');
}
merged.forEach(item => {
if (item.movieId && !/^(db|dj|im)_/.test(item.movieId)) {
if (item.subjectUrl && (item.subjectUrl.indexOf('hongguoduanju') !== -1 || item.subjectUrl.indexOf('duanjubaike') !== -1)) item.movieId = 'dj_' + item.movieId;
else if (item.subjectUrl && item.subjectUrl.indexOf('imoviebot') !== -1) item.movieId = 'im_' + item.movieId;
else item.movieId = 'db_' + item.movieId;
}
});
saveUnifiedHistory(merged);
// 2) 进度键迁移 + 旧搜索缓存清理
const allKeys = GM_listValues();
allKeys.forEach(key => {
let m = key.match(/^douban_movie_(.+)$/);
if (m) { const d = GM_getValue(key, null); if (d) { GM_setValue('play_db_' + m[1], d); } GM_deleteValue(key); return; }
m = key.match(/^dj_drama_(.+)$/);
if (m) { const d = GM_getValue(key, null); if (d) { GM_setValue('play_dj_' + m[1], d); } GM_deleteValue(key); return; }
m = key.match(/^dj_search_(.+)$/);
if (m) { GM_deleteValue(key); return; } // 旧短剧搜索缓存直接丢弃
m = key.match(/^search_(\d+)_/);
if (m) { GM_deleteValue(key); return; } // 旧搜索缓存直接丢弃
});
// 3) 设置项合并
const djAuto = GM_getValue('dj_autoPlayNextEnabled', undefined);
if (djAuto !== undefined && GM_getValue(AUTO_NEXT_ENABLED_KEY, undefined) === undefined) GM_setValue(AUTO_NEXT_ENABLED_KEY, djAuto);
GM_deleteValue('dj_autoPlayNextEnabled');
GM_setValue(MIGRATION_KEY, true);
} catch (e) { console.warn('[阿豆整合] 数据迁移失败', e); }
}
// 预加载播放数据(从搜索结果直接启动播放器时使用)
let cmsPlayData = null;
// ==================== CMS 搜索协调器(单飞 + 取消 + 订阅) ====================
// 目标:同一关键词同时只发一路 CMS 聚合搜索;弹窗/播放器/历史播放可订阅进度;
// 订阅者全部退出后延迟取消底层请求,避免"弹窗关闭后仍打站"与"两路并发限流"。
const tip = (message, duration = 3000) => {
try {
const old = document.getElementById('custom-tip');
if (old) old.remove();
const el = document.createElement('div');
el.id = 'custom-tip';
el.className = 'adys-tip';
el.innerText = message;
document.body.appendChild(el);
setTimeout(() => el.remove(), duration);
} catch (e) { alert(message); }
};
const escapeHtml = (str) => {
if (!str) return '';
return str.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]));
};
const htmlToElement = (html) => {
try {
const t = document.createElement('template');
t.innerHTML = html.trim();
return t.content.firstChild;
} catch (e) { tip('界面生成失败,请刷新页面重试'); return null; }
};
const get = (detail, retryCount = 0, timeoutMs = 10000, signal = null) => {
const maxRetry = 2;
const backoff = (r) => Math.pow(2, r) * 1000;
return new Promise((resolve) => {
if (signal && signal.aborted) { resolve({ r: false, errorMsg: '请求已取消' }); return; }
const timer = setTimeout(() => resolve({ r: false, errorMsg: '请求超时' }), timeoutMs);
const abortHandler = () => { clearTimeout(timer); resolve({ r: false, errorMsg: '请求已取消' }); };
if (signal) signal.addEventListener('abort', abortHandler, { once: true });
const cfg = {
method: 'GET', timeout: timeoutMs,
headers: { 'User-Agent': navigator.userAgent, 'Accept': '*/*', 'Cache-Control': 'no-cache' },
anonymous: true, followRedirects: true,
onload: (r) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (r.status >= 400) { resolve({ r: false, errorMsg: `请求失败(${r.status})` }); return; } resolve({ r: true, content: r.response, status: r.status, errorMsg: '请求成功' }); },
onerror: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (retryCount < maxRetry) setTimeout(() => resolve(get(detail, retryCount + 1, timeoutMs, signal)), backoff(retryCount)); else resolve({ r: false, errorMsg: '网络异常' }); },
onabort: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); resolve({ r: false, errorMsg: '请求中止' }); },
ontimeout: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (retryCount < maxRetry) setTimeout(() => resolve(get(detail, retryCount + 1, timeoutMs, signal)), backoff(retryCount)); else resolve({ r: false, errorMsg: '请求超时' }); },
};
GM_xmlhttpRequest(Object.assign(cfg, detail));
});
};
const fetchWithTimeout = (url, timeout = 15000, signal = null) => {
// get() 内部已通过 AbortSignal 处理超时/中止并返回 {r:false,errorMsg:...},这里不再叠加一层 Promise.race,
// 避免外层先超时后内层 GM 请求仍在后台运行,造成连接浪费与站点限流。
// signal: 可选 AbortSignal,用于在搜索被取消(弹窗关闭/换词/播放让路)时中止未完成请求。
return get({ url: encodeURI(url), responseType: 'json', overrideMimeType: 'application/json' }, 0, timeout, signal)
.catch(err => ({ r: false, errorMsg: err && err.message ? err.message : String(err) }));
};
// ==================== 通用小工具(多处复用,避免重复实现)====================
// og:image 海报回退
const getOgImage = () => {
const og = document.querySelector('meta[property="og:image"]');
return (og && og.content) ? og.content : '';
};
// 播放进度百分比(历史浮窗与播放器历史共用)
const calcProgressPercent = (playHistory) => {
if (!playHistory) return 0;
const ct = playHistory.currentTime || 0;
const dur = playHistory.duration || 0;
return dur > 0 ? Math.min(Math.round((ct / dur) * 100), 100) : 0;
};
// 清除全部观看历史并提示(菜单命令与设置面板共用)
const doClearHistory = () => {
clearAllUnifiedHistory();
tip('所有观看历史已清除');
};
// 重置"一键搜索"按钮外观(电脑版/手机版共用)
const resetBtnEl = (btn) => {
if (!btn) return;
btn.innerText = '一键搜索';
btn.style.backgroundColor = '';
btn.disabled = false;
btn.style.cursor = 'pointer';
};
// 防抖 MutationObserver 工厂:高频 DOM 变动合并为一次回调,避免同步遍历热点
const observeDebounced = (target, callback, { delay = 300, options } = {}) => {
let timer = null;
const mo = new MutationObserver(() => {
if (timer) return;
timer = setTimeout(() => {
timer = null;
try { callback(); } catch (e) {}
}, delay);
});
mo.observe(target, options || { childList: true, subtree: true });
return mo;
};
// ==================== 匹配算法 ====================
const _noiseCache = new Map();
const removeNoiseWords = (str) => {
if (!str) return '';
if (_noiseCache.has(str)) return _noiseCache.get(str);
let r = str.replace(/[((【\[][^)\])】]*?(全集|高清|4K|8K|蓝光|修复版|国语|中字|BD|HD)[^)\])】]*?[))】\]]/gi, '').trim();
r = r.replace(/(全集|高清|4K|8K|蓝光|修复版|国语|中字|中文字幕|无水印|无删减|完整版|加长版|导演剪辑版|未删减|BD|HD|WEB-DL|WEBRip|BluRay|DVDRip)$/gi, '').trim();
if (_noiseCache.size > 500) _noiseCache.clear();
_noiseCache.set(str, r);
return r;
};
// ==================== 爱看搜索(双站:爱看1 / 爱看2) ====================
// base: 站点根地址,如 https://www1.ikanbot.com 或 https://ikanbot.eu.org
// label: 结果标签,如 爱看1 / 爱看2
// ISS-004:默认 https(防中间人/混合内容),站点仅支持 http 时回退一次明文请求,功能不中断
const searchAikan = async (keyword, base, label) => {
base = base || 'https://ikanbot.eu.org';
label = label || '爱看';
const encQ = encodeURIComponent(keyword);
let resp = await get({ url: `${base}/search?q=${encQ}`, responseType: 'text', anonymous: false }, 2, 15000);
if (!resp.r) {
const httpBase = base.replace(/^https/, 'http');
const httpResp = await get({ url: `${httpBase}/search?q=${encQ}`, responseType: 'text', anonymous: false }, 1, 15000);
if (httpResp.r) { resp = httpResp; base = httpBase; }
}
if (!resp.r) return [];
const html = resp.content;
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const items = doc.querySelectorAll('.media');
const results = [];
items.forEach(item => {
const linkEl = item.querySelector('.media-left a.cover-link');
const imgEl = item.querySelector('.media-left img');
const titleEl = item.querySelector('.media-body h5 a.title-text');
const labelEl = item.querySelector('.media-body .label');
const smallEls = item.querySelectorAll('.media-body .small');
if (!titleEl || !linkEl) return;
const title = titleEl.textContent.trim();
const urlPath = linkEl.getAttribute('href');
if (!urlPath) return;
const fullUrl = /^https?:\/\//i.test(urlPath) ? urlPath : `${base}${urlPath}`;
let poster = imgEl ? (imgEl.getAttribute('data-src') || imgEl.getAttribute('src') || '') : '';
if (poster && poster.startsWith('data:image')) poster = '';
let lineCount = '';
if (labelEl) {
const text = labelEl.textContent.trim();
const match = text.match(/\[(\d+)条线路可播放\]/);
if (match) lineCount = match[1] + '条线路';
}
let abstract = '';
if (smallEls.length > 0) {
abstract = smallEls[0].textContent.trim();
if (smallEls.length > 1) {
abstract += ' ' + smallEls[1].textContent.trim();
}
}
results.push({
title: title,
url: fullUrl,
poster: poster,
abstract: abstract,
source: 'aikan',
siteLabel: label,
lineCount: lineCount,
});
});
return results.slice(0, 50);
};
// 爱看:从播放页 HTML 中提取 current_id / e_token / mtype
const parseAikanPlayPage = (html) => {
const doc = new DOMParser().parseFromString(html, 'text/html');
const g = (id) => { const el = doc.getElementById(id); return el ? el.value : ''; };
return { id: g('current_id'), token: g('e_token'), mtype: g('mtype') || '1' };
};
// 爱看:按播放页 get_tks 算法计算接口 token(与站方 play_new.js 一致)
const calcAikanTks = (videoId, eToken) => {
if (!videoId || !eToken) return '';
const tail = videoId.substring(videoId.length - 4, videoId.length);
let tok = eToken;
const parts = [];
for (let i = 0; i < tail.length; i++) {
const digit = parseInt(tail[i], 10);
if (isNaN(digit)) continue;
const pos = digit % 3 + 1;
parts.push(tok.substring(pos, pos + 8));
tok = tok.substring(pos + 8, tok.length);
}
return parts.join('');
};
// 爱看:把 /api/getResN 返回的线路列表构造为内置播放器 sources
const buildAikanSources = (resJson, title) => {
const list = (resJson && resJson.data && resJson.data.list) || [];
const sources = [];
list.forEach((item, idx) => {
let resArr;
try { resArr = typeof item.resData === 'string' ? JSON.parse(item.resData) : item.resData; }
catch (e) { return; }
if (!Array.isArray(resArr)) return;
const playList = [];
resArr.forEach(res => {
const raw = (res && res.url) || '';
raw.split('#').forEach(seg => {
const i = seg.indexOf('$');
if (i < 0) return;
const name = seg.slice(0, i).trim();
const url = seg.slice(i + 1).trim();
if (url && /\.m3u8($|\?)/i.test(url)) {
playList.push({ name: name || ('第' + (playList.length + 1) + '集'), url, speed: -1 });
}
});
});
if (playList.length > 0) sources.push({ name: '线路' + (idx + 1), playList, vod_name: title, vod_pic: '' });
});
return sources;
};
// 爱看搜索结果 → 拉取播放页线路 → 塞进内置播放器(上方播放器 + 下方选集/线路)
async function playAikanInPlayer(r) {
const playUrl = r.url;
if (!playUrl) { tip('该结果无可播放地址'); return; }
let origin = '';
try { origin = new URL(playUrl).origin; } catch (e) { tip('链接解析失败'); return; }
const tipEl = showLoadingTip('正在获取爱看线路...', 0, 0, 0);
try {
// 1) 拉播放页 HTML,拿 current_id / e_token / mtype
const pageResp = await get({ url: playUrl, responseType: 'text', anonymous: false }, 2, 15000);
if (!pageResp.r || !pageResp.content) throw new Error('播放页加载失败');
const meta = parseAikanPlayPage(pageResp.content);
if (!meta.id || !meta.token) throw new Error('未找到播放参数');
// 2) 计算 token 并请求线路接口
const tks = calcAikanTks(meta.id, meta.token);
const apiUrl = `${origin}/api/getResN?videoId=${encodeURIComponent(meta.id)}&mtype=${encodeURIComponent(meta.mtype)}&token=${encodeURIComponent(tks)}`;
const apiResp = await get({ url: apiUrl, responseType: 'text', anonymous: false, headers: { 'Referer': playUrl } }, 2, 15000);
if (!apiResp.r || !apiResp.content) throw new Error('线路接口请求失败');
let resJson;
try { resJson = JSON.parse(apiResp.content); } catch (e) { throw new Error('线路数据解析失败'); }
if (resJson.state !== 1) throw new Error('线路获取失败(' + resJson.state + ')');
// 3) 构造 sources 并启动内置播放器
const sources = buildAikanSources(resJson, r.title);
if (sources.length === 0) throw new Error('未解析到有效 m3u8 线路');
hideLoadingTip();
const uid = 'im_' + meta.id;
addToUnifiedHistory(uid, r.title, playUrl, {}, r.poster || '');
cmsPlayData = { title: r.title, poster: r.poster || '', sources: sources, uid: uid };
try { initVue(); } catch (err) { tip('播放器启动失败: ' + err.message); }
} catch (err) {
hideLoadingTip();
tip(err.message || '加载失败');
}
};
// ==================== 播放辅助 ====================
const playM3u8 = (video, url, art) => {
try {
if (Hls.isSupported()) {
if (art.hls) art.hls.destroy();
const hls = new Hls({ maxBufferLength: 60, maxMaxBufferLength: 120, startLevel: -1, enableWorker: true, backBufferLength: 30 });
hls.loadSource(url);
hls.attachMedia(video);
art.hls = hls;
art.on('destroy', () => hls.destroy());
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
} else {
art.notice.show = '不支持的播放格式: m3u8';
}
} catch (e) { tip('视频播放初始化失败,请重试'); }
};
const downloadtsList = async (url, signal = null) => {
if (!url) return { r: false };
try {
const baseUrl = new URL(url);
const result = await get({ url: encodeURI(url) }, 0, 10000, signal);
if (!result.r) return { r: false };
const content = result.content.trim().replace(/^\uFEFF/, '');
if (!content.includes('#EXTM3U')) return { r: false };
if (content.includes('#EXT-X-STREAM-INF')) {
const lines = content.split('\n');
const list = [];
let lastBw = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#EXT-X-STREAM-INF')) { const m = line.match(/BANDWIDTH=(\d+)/); lastBw = m ? parseInt(m[1]) : 0; }
else if (line && !line.startsWith('#') && line.includes('.m3u8')) list.push({ url: new URL(line, baseUrl).href, bandwidth: lastBw });
}
if (list.length > 0) { list.sort((a, b) => b.bandwidth - a.bandwidth); return await downloadtsList(list[0].url, signal); }
}
if (content.includes('.ts')) {
const tsList = [];
for (const item of content.split('\n')) {
const t = item.trim();
if (/^[#\s]/.test(t) || t === '') continue;
tsList.push(new URL(t, baseUrl).href);
}
return { r: true, content: tsList };
}
return { r: false };
} catch (e) { return { r: false }; }
};
const showLoadingTip = (text, successCount = 0, totalCount = 0, failedCount = 0) => {
try {
const old = $('#loading-tip');
if (old) old.remove();
let progressHtml = '';
if (totalCount > 0) {
const completed = successCount + failedCount;
const pct = Math.round((completed / totalCount) * 100);
progressHtml = `
成功:${successCount} / 失败:${failedCount} / 共 ${totalCount}
`;
}
const el = htmlToElement(`${text}${progressHtml}
`);
document.body.appendChild(el);
return el;
} catch (e) { return null; }
};
const hideLoadingTip = () => { try { const el = $('#loading-tip'); if (el) el.remove(); } catch (e) {} };
const resetPlayBtn = () => {
// 重置电脑版一键搜索按钮
resetBtnEl(window.doubanPlayBtn);
// 手机版浮动按钮组
const mobileGroup = document.getElementById('mobile-action-buttons');
if (mobileGroup && isMobile) {
mobileGroup.style.display = 'flex';
resetBtnEl(mobileGroup.querySelector('.play-btn'));
}
};
const asyncPool = async (limit, array, iteratorFn) => {
const ret = [];
const executing = [];
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p);
if (limit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
const jitter = Math.floor(Math.random() * (ANTI_BLOCK_CONFIG.requestJitterMax - ANTI_BLOCK_CONFIG.requestJitterMin + 1)) + ANTI_BLOCK_CONFIG.requestJitterMin;
await new Promise(r => setTimeout(r, jitter));
}
}
}
return Promise.all(ret);
};
/* ==================== Part 2:界面、逻辑与初始化 ==================== */
// ==================== 1. CSS 样式(播放器界面)====================
GM_addStyle(`
/* ================= 设计系统 · 主题令牌 ================= */
:root {
/* 字阶体系(断崖式) */
--adys-fs-xs: 11px;
--adys-fs-sm: 13px;
--adys-fs-md: 15px;
--adys-fs-lg: 18px;
--adys-fs-xl: 22px;
/* 圆角角色 */
--adys-r-pill: 999px;
--adys-r-md: 8px;
--adys-r-lg: 12px;
--adys-r-xl: 16px;
/* 动效 */
--adys-ease: cubic-bezier(.2,.8,.2,1);
--adys-fast: .16s;
--adys-med: .24s;
}
/* 主题变量 - 深色(默认);播放器层 .liu-playContainer 始终强制深色,不随浅色主题漂白 */
:root, [data-adys-theme="dark"], .liu-playContainer {
--adys-bg: #1b1f24;
--adys-bg-deep: #13161b;
--adys-bg-elev: #222830;
--adys-bg-hover: #2a323c;
--adys-border: #333c47;
--adys-text: #f5f7fa;
--adys-text-dim: #9aa4b2;
--adys-text-sub: #6c7786;
--adys-accent: #00b96b;
--adys-accent-hover: #00d67c;
--adys-accent-light: #4dd794;
--adys-accent-soft: rgba(0,185,107,.15);
--adys-danger: #ff6b6b;
--adys-warn: #ffb020;
--adys-ring: rgba(0,185,107,.35);
--adys-overlay: rgba(7,9,13,.72);
}
/* 主题变量 - 浅色 */
[data-adys-theme="light"] {
--adys-bg: #f6f7f9;
--adys-bg-deep: #ffffff;
--adys-bg-elev: #ffffff;
--adys-bg-hover: #edeff3;
--adys-border: #e3e6eb;
--adys-text: #1b1f24;
--adys-text-dim: #5f6a78;
--adys-text-sub: #9aa4b2;
--adys-accent: #009a5a;
--adys-accent-hover: #00b96b;
--adys-accent-light: #17b574;
--adys-accent-soft: rgba(0,154,90,.12);
--adys-danger: #e5484d;
--adys-warn: #d98b00;
--adys-ring: rgba(0,154,90,.3);
--adys-overlay: rgba(24,28,34,.42);
}
:root::-webkit-scrollbar, .liu-playContainer::-webkit-scrollbar, .series-contianer::-webkit-scrollbar { display: none; }
.TalionNav{ z-index:10; }
/* Artplayer 自定义控制按钮样式 */
.art-control.art-control-prev-episode, .art-control.art-control-next-episode {
font-size: 20px !important;
padding: 0 8px !important;
cursor: pointer !important;
opacity: 0.85;
transition: opacity 0.2s;
}
.art-control.art-control-prev-episode:hover, .art-control.art-control-next-episode:hover {
opacity: 1;
}
/* 确保 Artplayer 播放器层级高于底部搜索栏,防止控制栏被遮挡 */
.art-video-player {
z-index: 1000000 !important;
}
/* 播放器打开时:隐藏手机浮动按钮(一键搜索/爱看影视),用 body 类 + !important 确保生效 */
body.adys-playing #mobile-action-buttons {
display: none !important;
}
/* Artplayer 网页全屏时隐藏底部搜索栏,防止遮挡控制栏(CSS 方案,兼容 :has) */
body:has(.art-video-player.art-fullscreen-web) .search-container,
body.adys-fullscreen-web .search-container {
display: none !important;
}
/* 手机版:播放器容器内 artplayer-app 层级高于底部搜索栏 */
@media screen and (max-width: 768px) {
.player-panel { position: relative; z-index: 1000001; }
/* 确保 Artplayer 控制栏不被容器裁剪 */
.artplayer-app { overflow: visible !important; }
.artplayer-app .art-video-player { overflow: hidden !important; border-radius: 6px; }
}
.speed-slow{ color:#9e9e9e; }
.speed-fast{ color:var(--adys-accent-light); }
.speed-testing{ color:#ff4d4d !important; font-weight:500; }
.failed-status{ color:#F76965 !important; font-weight: 500; }
.mannul{ margin:16px 0px 16px 14px; font-size:16px; display:flex; flex-wrap:wrap; }
.authoralert{ font-size:16px; margin-left:14px; color:#F76965; }
.liu-btn{ cursor:pointer; font-size:17px; padding: 12px 18px; border: 1px solid transparent; border-radius: 6px; max-height:60px; box-sizing: border-box; }
/* 按钮容器:确保多个按钮在同一水平线 */
.adys-btn-group { display: inline-flex !important; align-items: center; gap: 12px; flex-wrap: wrap; }
.play-btn { border-radius: 8px; cursor: pointer; font-weight: bolder; background-color:#e8f5e9; color:#1a1a1a !important; border: none; display: inline-flex !important; align-items: center; justify-content: center; }
.play-btn:hover { background-color:#c8e6c9; }
.play-btn:active{ background-color: #81c784; }
.play-btn:disabled { background-color:#9e9e9e; cursor:not-allowed; }
/* 电脑版红果详情:顶替原生"播放正片"按钮的样式 */
.dj-hero-play { height:49px !important; padding:10px 40px !important; font-size:18px !important; font-weight:500 !important; letter-spacing:1px; background:linear-gradient(105.2deg,#ff7e0d .62%,#ff9233) !important; color:#fff !important; border-radius:12px !important; width:auto !important; min-width:200px; flex:0 0 auto; margin:0 !important; }
.dj-hero-play:hover { background:linear-gradient(105.2deg,#ff7e0d .62%,#ff9233) !important; filter:brightness(1.06); }
.dj-hero-play:active { background:linear-gradient(105.2deg,#ff7e0d .62%,#ff9233) !important; filter:brightness(0.96); }
/* 手机版独立按钮组 - 固定在右下角圆形按钮上方 */
.mobile-action-buttons {
position: fixed;
bottom: calc(30px + 48px + 12px + env(safe-area-inset-bottom, 0px));
right: 20px;
z-index: 10000000;
display: flex;
gap: 12px;
flex-direction: row;
justify-content: flex-end;
}
.mobile-action-buttons .play-btn {
margin: 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.liu-closePlayer{
position: absolute;
top: 10px;
right: 10px;
z-index: 99999;
border-radius: 50%;
background-color: rgba(0,0,0,0.7) !important;
color: #ffffff !important;
width: 36px;
height: 36px;
line-height: 36px;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
transition: all 0.2s ease;
}
.liu-closePlayer:hover{
background-color: rgba(0,112,17,0.9) !important;
transform: scale(1.05);
}
.source-selector{
width: 130px;
height: 56px;
padding: 6px 8px;
margin: 0 10px 10px 0;
border-radius: var(--adys-r-lg);
flex-shrink: 0;
background-color: var(--adys-bg-deep) !important;
color: var(--adys-text-dim) !important;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
line-height: 1.2;
box-sizing: border-box;
border: 2px solid transparent;
position: relative;
transition: border-color var(--adys-fast) var(--adys-ease), background-color var(--adys-fast) var(--adys-ease), transform var(--adys-fast) var(--adys-ease);
}
.source-selector:hover:not(:disabled) {
background-color: var(--adys-bg-hover) !important;
transform: translateY(-1px);
}
.source-selector.selected {
border-color: var(--adys-accent);
background-color: var(--adys-accent-soft) !important;
}
.source-selector.selected::after {
content: '✓';
position: absolute;
top: 4px;
right: 8px;
color: var(--adys-accent-light);
font-size: 13px;
font-weight: 700;
line-height: 1;
}
.source-name {
font-size: 14px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.source-status {
font-size: 11px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
}
.source-status.speed-fast {
color: var(--adys-accent-light);
}
.source-status.speed-slow {
color: #9e9e9e;
}
.source-status.speed-testing {
color: #ff4d4d !important;
}
.pc-source-list {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
padding: 5px 0;
margin: 0;
scroll-behavior: smooth;
width: 100%;
box-sizing: border-box;
}
.pc-source-list::-webkit-scrollbar {
height: 4px;
}
.pc-source-list::-webkit-scrollbar-thumb {
background-color: var(--adys-accent);
border-radius: 4px;
}
.mobile-only .sourceButtonList {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px;
padding:0 16px;
box-sizing: border-box;
overflow-y: auto;
max-height: 200px;
}
.mobile-only .sourceButtonList .source-selector {
width: 100%;
margin: 0;
height: 48px;
padding: 4px 4px;
}
.mobile-only .sourceButtonList .source-selector .source-name {
font-size: 12px;
}
.mobile-only .sourceButtonList .source-selector .source-status {
font-size: 10px;
}
.series-selector{ background-color: var(--adys-bg-elev) !important; border-radius: var(--adys-r-md); color: var(--adys-text-dim) !important; font-size: var(--adys-fs-sm); padding: 10px 8px; box-sizing: border-box; border: 2px solid var(--adys-border); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: border-color var(--adys-fast) var(--adys-ease), background-color var(--adys-fast) var(--adys-ease), color var(--adys-fast) var(--adys-ease); }
.series-selector:hover{ background-color: var(--adys-accent-soft) !important; box-sizing: border-box; color: var(--adys-text) !important; }
.playing{ border:2px solid var(--adys-accent); background-color: var(--adys-accent-soft) !important; color: var(--adys-accent-light) !important; box-sizing: border-box; }
.love-support{ color:var(--adys-text-dim); background-color:transparent; margin-right:32px; }
.liu-playContainer a:visited{ color:var(--adys-text-dim); }
.liu-playContainer a:hover{ font-weight:bold; color:#A8DB39; background:none; }
.series-contianer{
display:grid;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
grid-auto-rows:44px;
grid-column-gap:10px;
grid-row-gap:10px;
margin-top:16px;
height:400px;
overflow-y:auto;
scroll-behavior: smooth;
position: relative;
box-sizing: border-box;
padding-right: 4px;
}
.series-wrapper {
position: relative;
}
.series-wrapper.has-more::after {
content: "▼";
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
color: var(--adys-text-dim);
font-size: 14px;
z-index: 2;
animation: bounce 2s ease-in-out 8;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.series-wrapper.has-more::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30px;
background: linear-gradient(transparent, var(--adys-bg-deep));
z-index: 1;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.series-wrapper.has-more::after,
.series-wrapper.has-more::before {
opacity: 1;
}
@keyframes bounce {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(4px); }
}
@keyframes scrollTip {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.liu-playContainer{
width: 100vw !important;
width: 100dvw !important;
height: 100vh !important;
height: 100dvh !important;
background-color:var(--adys-bg);
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 999999 !important;
overflow: auto;
padding-bottom: 60px;
box-sizing: border-box;
padding-top: 40px;
margin: 0 !important;
font-size: 16px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.liu-playContainer * {
box-sizing: border-box;
}
.seletor-title{
height:50px;
line-height:50px;
background-color: var(--adys-bg-deep);
color:var(--adys-text);
font-size: var(--adys-fs-lg);
padding:0 16px;
border-radius:6px 6px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.title-text {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-right-actions {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.history-btn {
background: rgba(74, 161, 80, 0.15) !important;
border: 1.5px solid var(--adys-accent-light) !important;
color: var(--adys-accent-light) !important;
padding: 4px 14px !important;
font-size: var(--adys-fs-lg) !important;
line-height: 1.2 !important;
border-radius: 24px !important;
transition: all 0.2s ease;
max-height: 38px;
display: inline-flex;
align-items: center;
font-weight: 500;
}
.history-btn:hover {
background: var(--adys-accent-light) !important;
color: #fff !important;
transform: scale(1.02);
}
.speed-test-btn {
background-color: var(--adys-accent) !important;
color: white !important;
margin: 0;
width: 100%;
max-width: 200px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: var(--adys-r-md);
white-space: nowrap; /* 文字在"开始测速/测速中 X/Y"间切换时不换行,高度恒定,避免上下边距抖动收缩 */
min-height: 44px;
box-sizing: border-box;
}
.speed-test-btn:disabled {
background-color: #9e9e9e !important;
cursor: not-allowed;
}
.speed-test-btn:hover:not(:disabled) {
background-color: var(--adys-accent-hover) !important;
}
/* 测速中:仅保留文字进度反馈(“测速中 X/Y”),不叠加额外动画元素 */
.control-btn-group {
display: flex;
align-items: center;
gap: 0;
margin-bottom:12.8px;
}
.mobile-btn-group {
display: flex;
justify-content: center;
align-items: center;
gap:16px;
padding:0 16px;
}
.pc-only {
display: none;
}
.mobile-only {
display: block;
}
.search-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
/* 播放器内固定深色:不随浅色主题漂白,保证与深色播放器一体 */
background-color: #14171c;
padding:16px;
box-sizing: border-box;
box-shadow: 0 -2px 10px rgba(0,0,0,0.5);
display: flex;
gap:8px;
z-index: 999999;
}
.search-input {
flex: 1;
padding:12.8px 16px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.14);
background-color: rgba(255,255,255,0.07);
color: #e8eaed;
font-size:16px;
outline: none;
}
.search-input::placeholder {
color: rgba(255,255,255,0.42);
}
.search-input:focus {
border-color: var(--adys-accent);
}
.search-btn {
background-color: var(--adys-accent);
color: white;
white-space: nowrap;
flex-shrink: 0;
}
.search-btn:hover:not(:disabled) {
background-color: var(--adys-accent-hover);
}
.search-btn:disabled {
background-color: #9e9e9e;
cursor: not-allowed;
}
.loading-tip {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--adys-bg-deep);
color: var(--adys-text);
padding:19.2px 32px;
border-radius: 12px;
z-index: 1000000;
font-size:16px;
text-align: center;
line-height: 1.5;
max-width: 80vw;
border: 1px solid var(--adys-border);
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
}
.loading-tip .rate-success { color: var(--adys-accent-light); font-weight: bold; }
.loading-tip .rate-failed { color: #F76965; font-weight: bold; }
.loading-tip .loading-progress-wrap {
margin-top: 10px;
width: 200px;
height: 6px;
background: var(--adys-border);
border-radius: 3px;
overflow: hidden;
}
.loading-tip .loading-progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--adys-accent), var(--adys-accent-light));
border-radius: 3px;
transition: width 0.4s ease;
width: 0%;
}
.loading-tip .loading-stats {
margin-top: 8px;
font-size: var(--adys-fs-sm);
color: var(--adys-text-dim);
}
/* Toast 提示 */
.adys-tip {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--adys-bg-deep);
color: var(--adys-text);
padding: 12px 24px;
border-radius: var(--adys-r-lg);
z-index: 9999999;
font-size: var(--adys-fs-md);
line-height: 1.5;
text-align: center;
max-width: 80vw;
pointer-events: none;
box-shadow: 0 8px 30px rgba(0,0,0,.35);
border: 1px solid var(--adys-border);
animation: adysPopIn var(--adys-fast) var(--adys-ease);
}
/* 弹窗统一动效 */
@keyframes adysFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes adysPopIn { from { opacity: 0; transform: translateY(14px) scale(.965); } to { opacity: 1; transform: translateY(0) scale(1); } }
.history-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--adys-overlay);
z-index: 10000000;
display: flex;
align-items: center;
justify-content: center;
animation: adysFadeIn var(--adys-med) var(--adys-ease);
}
.history-modal-content {
background: var(--adys-bg);
border-radius: var(--adys-r-xl);
width: 90%;
max-width: 480px;
max-height: 70vh;
display: flex;
flex-direction: column;
box-shadow: 0 12px 40px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.15);
border: 1px solid var(--adys-border);
animation: adysPopIn var(--adys-med) var(--adys-ease);
font-size: 16px;
line-height: 1.5;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.history-modal-content * {
box-sizing: border-box;
}
.history-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--adys-border);
font-size: var(--adys-fs-lg);
font-weight: 500;
color: var(--adys-text);
}
.history-modal-close {
background: none;
border: none;
color: var(--adys-text-dim);
font-size: 28px;
cursor: pointer;
line-height: 1;
padding: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.history-modal-close:hover {
background: var(--adys-bg-hover);
color: var(--adys-text);
}
.history-modal-body {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
@media (min-width: 1025px) {
.history-modal-body {
scrollbar-width: thin;
scrollbar-color: var(--adys-accent-light) var(--adys-bg-hover);
}
.history-modal-body::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.history-modal-body::-webkit-scrollbar-track {
background: var(--adys-bg-hover);
border-radius: 4px;
}
.history-modal-body::-webkit-scrollbar-thumb {
background: var(--adys-accent-light);
border-radius: 4px;
}
.history-modal-body::-webkit-scrollbar-thumb:hover {
background: var(--adys-accent-hover);
}
}
@media (max-width: 1024px) {
.history-modal-body {
scrollbar-width: none;
-ms-overflow-style: none;
}
.history-modal-body::-webkit-scrollbar {
display: none;
}
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid var(--adys-bg-hover);
transition: background 0.2s;
gap: 12px;
}
.history-item:hover {
background: var(--adys-bg-hover);
}
.history-item-poster {
width: 48px;
height: 60px;
border-radius: 6px;
object-fit: cover;
flex-shrink: 0;
background: var(--adys-bg-hover);
}
.history-item-poster-placeholder {
width: 48px;
height: 60px;
border-radius: 6px;
flex-shrink: 0;
background: var(--adys-bg-hover);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--adys-fs-xl);
color: var(--adys-text-dim);
}
.hist-ph-icon {
width: 26px;
height: 26px;
}
.history-item-info {
flex: 1;
cursor: pointer;
overflow: hidden;
}
.history-item-name {
font-size: var(--adys-fs-md);
color: var(--adys-text);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item-meta {
font-size: var(--adys-fs-xs);
color: var(--adys-text-dim);
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.history-item-progress {
width: 60px;
height: 4px;
background: var(--adys-bg-hover);
border-radius: 2px;
overflow: hidden;
display: inline-block;
}
.history-item-progress-bar {
height: 100%;
background: var(--adys-accent-light);
border-radius: 2px;
transition: width 0.3s;
}
.history-item-episode {
color: var(--adys-accent-light);
font-size: var(--adys-fs-xs);
}
.history-item-time {
font-size: var(--adys-fs-xs);
color: var(--adys-text-dim);
}
.history-item-delete {
background: none;
border: none;
color: var(--adys-text-dim);
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: all var(--adys-fast) var(--adys-ease);
opacity: 0.65;
display: inline-flex;
align-items: center;
justify-content: center;
}
.history-item-delete .hist-del-icon {
width: 18px;
height: 18px;
}
.history-item-delete:hover {
background: var(--adys-danger);
color: #fff;
opacity: 1;
}
.history-modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-top: 1px solid var(--adys-border);
font-size:12px;
color: var(--adys-text-dim);
}
.history-tip {
font-size: var(--adys-fs-xs);
}
.history-clear-all {
background: transparent;
border: 1px solid var(--adys-danger);
color: var(--adys-danger);
border-radius: 16px;
padding: 4px 12px;
font-size:12px;
cursor: pointer;
transition: all 0.2s;
}
.history-clear-all:hover {
background: var(--adys-danger);
color: #fff;
}
.history-empty {
text-align: center;
padding: 40px 20px;
color: var(--adys-text-dim);
}
/* 全局按钮容器 - 按钮水平排列 */
#global-buttons-container {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 12px;
z-index: 10000000 !important;
font-size: 16px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#global-buttons-container * {
box-sizing: border-box;
}
.global-history-btn {
width: 56px;
height: 56px;
background-color: var(--adys-accent);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
transition: all 0.2s ease;
border: none;
}
.global-history-btn:hover {
background-color: var(--adys-accent-hover);
transform: scale(1.05);
}
.global-history-btn svg {
width: 26px;
height: 26px;
display: block;
}
/* 当前页面匹配的按钮高亮 */
.global-btn-active {
background-color: #ffac2c !important;
color: #1a1a1a !important;
border: 2px solid #ff8c00 !important;
box-shadow: 0 0 8px rgba(255, 172, 44, 0.6);
}
@media (max-width: 768px) {
#global-buttons-container {
bottom: calc(30px + env(safe-area-inset-bottom, 0px));
right: 16px;
gap: 8px;
}
.global-history-btn {
width: 48px;
height: 48px;
font-size: 20px;
}
.global-history-btn svg {
width: 22px;
height: 22px;
}
/* 手机版按钮组位置相应调整 */
.mobile-action-buttons {
bottom: calc(30px + 48px + 12px + env(safe-area-inset-bottom, 0px));
right: 16px;
}
/* 移动端扇形菜单... */
#global-buttons-container.mobile-fab-mode {
display: block;
}
#global-buttons-container.mobile-fab-mode .global-fab-item {
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
pointer-events: none;
transform: scale(0.5);
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 1;
}
#global-buttons-container.mobile-fab-mode.expanded .global-fab-item {
opacity: 1;
pointer-events: auto;
transform: scale(1);
}
#global-buttons-container.mobile-fab-mode .global-fab-main {
position: relative;
z-index: 2;
transition: transform 0.3s ease;
}
#global-buttons-container.mobile-fab-mode.expanded .global-fab-main {
transform: rotate(135deg);
}
/* 展开时子按钮显示文字标签 */
#global-buttons-container.mobile-fab-mode .global-fab-item::after {
content: attr(data-label);
position: absolute;
right: 58px;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 12px;
padding: 3px 10px;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease 0.1s;
}
#global-buttons-container.mobile-fab-mode.expanded .global-fab-item::after {
opacity: 1;
}
}
/* ========== 红果顶栏「热榜」入口 ========== */
a.hg-rank-entry {
flex: 0 0 auto;
color: #ff9500 !important;
font-size: 17px;
font-weight: 600;
padding: 4px 10px;
margin-left: 6px;
white-space: nowrap;
text-decoration: none;
}
/* 搜索浮窗样式 */
.search-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--adys-overlay);
z-index: 10000001;
display: flex;
align-items: center;
justify-content: center;
}
.search-modal-container {
background: var(--adys-bg);
border-radius: 16px;
width: 90%;
max-width: 500px;
max-height: 92vh;
padding:24px;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
border: 1px solid var(--adys-border);
display: flex;
flex-direction: column;
gap:12.8px;
overflow: hidden;
font-size: 16px;
line-height: 1.5;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.search-modal-container * {
box-sizing: border-box;
}
.search-modal-container .search-input-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.search-modal-container .search-btn {
flex-shrink: 0;
min-width: 80px;
}
.search-modal-container .search-input {
flex: 1;
min-width: 0; /* 关键:允许输入框收缩,按钮文字变长为"搜索中…"时不把按钮顶出弹窗右边界 */
background: var(--adys-bg-deep);
border: 1px solid var(--adys-border);
color: var(--adys-text);
font-size:16px;
padding:12.8px 16px;
border-radius: 8px;
outline: none;
}
.search-modal-container .search-input:focus {
border-color: var(--adys-accent);
}
.search-modal-container .search-btn {
background-color: var(--adys-accent);
color: white;
border: none;
padding:12.8px 24px;
border-radius: 8px;
font-size:16px;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
.search-modal-container .search-btn:hover {
background-color: var(--adys-accent-hover);
}
.search-history-section {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.search-history-label {
font-size: var(--adys-fs-sm);
color: var(--adys-text-dim);
flex-shrink: 0;
}
.search-history-tag {
background: var(--adys-bg-hover);
border: 1px solid var(--adys-border);
color: var(--adys-text);
padding: 4px 12px;
border-radius: 16px;
font-size:13.12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.search-history-tag:hover {
background: var(--adys-accent);
color: #fff;
border-color: var(--adys-accent);
}
.search-history-clear {
background: none;
border: none;
color: var(--adys-danger);
font-size:12px;
cursor: pointer;
margin-left: auto;
opacity: 0.7;
}
.search-history-clear:hover {
opacity: 1;
}
/* 搜索标签栏 - 高特异性选择器,防止宿主页CSS渗透 */
.search-modal-container .search-tab-bar {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: flex-end !important;
gap: 6px !important;
border-bottom: 2px solid var(--adys-border) !important;
padding: 4px 0 6px !important;
overflow-x: auto !important;
overflow-y: hidden !important;
-webkit-overflow-scrolling: touch;
width: 100% !important;
min-width: 0 !important;
position: relative !important;
z-index: 2 !important;
scrollbar-width: none !important;
background: var(--adys-bg) !important;
}
.search-modal-container .search-tab-bar::-webkit-scrollbar {
display: none !important;
}
.search-modal-container .search-tab-bar .search-tab {
background: rgba(255,255,255,0.08) !important;
border: none !important;
border-bottom: 2px solid transparent !important;
color: #ffffff !important;
font-size: 13px !important;
line-height: 1.35 !important;
padding: 8px 10px !important;
cursor: pointer !important;
margin-bottom: -2px !important;
transition: all 0.2s;
font-weight: 600 !important;
white-space: nowrap !important;
flex-shrink: 0 !important;
min-width: max-content !important;
min-height: 34px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
opacity: 1 !important;
float: none !important;
position: relative !important;
clear: none !important;
text-shadow: 0 1px 2px rgba(0,0,0,0.35) !important;
border-radius: 6px 6px 0 0 !important;
}
.search-modal-container .search-tab-bar .search-tab:hover {
background: rgba(255,255,255,0.14) !important;
color: #ffffff !important;
}
.search-modal-container .search-tab-bar .search-tab.active {
background: rgba(74,161,80,0.18) !important;
color: #6ee36e !important;
border-bottom-color: #6ee36e !important;
opacity: 1 !important;
font-weight: 700 !important;
}
.search-modal-container .search-tab-bar .search-tab-count {
font-size:12px !important;
opacity: 0.85 !important;
display: inline !important;
color: inherit !important;
}
/* 浅色主题下的标签颜色 */
[data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab {
background: rgba(0,0,0,0.05) !important;
color: #1c1c1e !important;
text-shadow: none !important;
}
[data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab:hover {
background: rgba(0,0,0,0.09) !important;
color: #1c1c1e !important;
}
[data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.active {
background: rgba(0,112,17,0.1) !important;
color: var(--adys-accent) !important;
border-bottom-color: var(--adys-accent) !important;
}
/* 搜索标签加载中状态(灰色不可点击) */
.search-modal-container .search-tab-bar .search-tab.loading {
color: var(--adys-text-dim) !important;
opacity: 0.85 !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
.search-modal-container .search-tab-bar .search-tab.loading .search-tab-count {
color: #6ee36e !important;
opacity: 1 !important;
font-weight: 700 !important;
}
[data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.loading {
color: #8e8e93 !important;
}
[data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.loading .search-tab-count {
color: var(--adys-accent) !important;
}
.search-modal-container .search-tab-bar .search-tab.loading .search-tab-count::after {
content: '';
}
.search-modal-container .search-tab-bar .search-tab.loaded-failed {
opacity: 0.7 !important;
}
.search-modal-container .search-tab-bar .search-tab.loaded-failed .search-tab-count::after {
content: ' (无结果)';
font-size: 11px;
}
/* 搜索结果区域 */
.search-results-section {
flex: 1 1 0;
min-height: 45vh;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.search-results-loading {
text-align: center;
padding:32px 16px;
color: var(--adys-text-dim);
font-size: var(--adys-fs-sm);
}
.search-results-loading .spinner {
display: inline-block;
width: 24px;
height: 24px;
border: 3px solid var(--adys-border);
border-top-color: var(--adys-accent);
border-radius: 50%;
animation: adys-spin 0.8s linear infinite;
margin-bottom: 8px;
}
@keyframes adys-spin { to { transform: rotate(360deg); } }
.search-result-card {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-radius: 10px;
background: var(--adys-bg-deep);
border: 1px solid var(--adys-border);
cursor: pointer;
transition: all 0.2s;
}
.search-result-card:hover {
border-color: var(--adys-accent);
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.search-result-card img {
width: 48px;
height: 64px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
background: var(--adys-bg-hover);
}
.search-result-card .result-placeholder {
width: 48px;
height: 64px;
border-radius: 6px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--adys-bg-hover);
font-size: var(--adys-fs-lg);
}
.search-result-card .result-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.search-result-card .result-title {
font-size: var(--adys-fs-md);
color: var(--adys-text);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
max-width: 60%;
}
.search-result-card .result-meta {
font-size:12px;
color: var(--adys-text-dim);
margin-top: 0;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.search-result-card .result-source {
display: inline-block;
padding: 1px 8px;
border-radius: 10px;
font-size:10.88px;
font-weight: 500;
}
.result-source.src-dj {
background: rgba(255, 172, 44, 0.15);
color: #ff8c00;
}
.result-source.src-db {
background: rgba(0, 152, 26, 0.15);
color: var(--adys-accent);
}
.result-source.src-cms {
background: rgba(74, 161, 80, 0.15);
color: var(--adys-accent-light);
margin-left: 4px;
}
.result-source.src-aikan {
background: rgba(255, 107, 107, 0.15);
color: #ff6b6b;
}
.result-source.src-hg {
background: rgba(255, 77, 79, 0.15);
color: #ff4d4f;
}
.result-abstract {
color: var(--adys-text-dim);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.result-play-btn {
flex-shrink: 0;
margin-left: auto;
padding: 6px 14px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 600;
background: var(--adys-accent-light);
color: #fff !important;
transition: all 0.2s;
white-space: nowrap;
align-self: center;
}
.result-play-btn:hover {
background: #3d8c43;
transform: scale(1.05);
}
.result-play-btn:active {
background: #2d6e33;
}
.search-results-empty {
text-align: center;
padding:24px 16px;
color: var(--adys-text-dim);
font-size: var(--adys-fs-sm);
}
/* ==================== 设置面板样式 ==================== */
.settings-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--adys-overlay);
z-index: 10000001;
display: flex;
align-items: center;
justify-content: center;
animation: adysFadeIn var(--adys-med) var(--adys-ease);
}
.settings-modal-container {
background: var(--adys-bg);
border-radius: var(--adys-r-xl);
width: 90%;
max-width: 520px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 12px 40px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.15);
border: 1px solid var(--adys-border);
animation: adysPopIn var(--adys-med) var(--adys-ease);
font-size: 16px;
line-height: 1.5;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.settings-modal-container * {
box-sizing: border-box;
}
.settings-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--adys-border);
font-size: var(--adys-fs-lg);
font-weight: 600;
color: var(--adys-text);
flex-shrink: 0;
}
.settings-header-title {
display: inline-flex;
align-items: center;
}
.settings-gear-icon {
width: 20px;
height: 20px;
margin-right: 8px;
color: var(--adys-text-dim);
}
.settings-modal-body {
flex: 1;
overflow-y: auto;
padding: 12px 20px;
}
.settings-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid var(--adys-bg-hover);
}
.settings-item:last-child {
border-bottom: none;
}
.settings-item-info {
flex: 1;
overflow: hidden;
}
.settings-item-title {
font-size:16px;
color: var(--adys-text);
font-weight: 500;
}
.settings-item-desc {
font-size:12px;
color: var(--adys-text-dim);
margin-top: 3px;
line-height: 1.4;
}
.settings-toggle {
position: relative;
width: 46px;
height: 26px;
border-radius: 13px;
background: #444;
border: none;
cursor: pointer;
transition: background 0.2s ease;
flex-shrink: 0;
padding: 0;
}
.settings-toggle.active {
background: var(--adys-accent);
}
.settings-toggle::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #fff;
transition: transform 0.2s ease;
}
.settings-toggle.active::after {
transform: translateX(20px);
}
.settings-action-btn {
background: transparent;
border: 1px solid var(--adys-accent-light);
color: var(--adys-accent-light);
border-radius: 20px;
padding: 6px 16px;
font-size: var(--adys-fs-sm);
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
flex-shrink: 0;
}
.settings-action-btn:hover {
background: var(--adys-accent-light);
color: #fff;
}
.settings-action-btn.danger {
border-color: var(--adys-danger);
color: var(--adys-danger);
}
.settings-action-btn.danger:hover {
background: var(--adys-danger);
color: #fff;
}
.settings-modal-footer {
padding: 12px 20px;
border-top: 1px solid var(--adys-border);
text-align: center;
flex-shrink: 0;
}
.settings-close-btn {
background: var(--adys-accent);
color: white;
border: none;
padding: 8px 32px;
border-radius: 20px;
font-size:16px;
cursor: pointer;
transition: background 0.2s;
}
.settings-close-btn:hover {
background: var(--adys-accent-hover);
}
/* 播放器内自动连播切换按钮 */
.autoplay-toggle-btn {
background: transparent !important;
border: none !important;
color: var(--adys-text) !important;
padding: 4px 8px !important;
font-size: var(--adys-fs-sm) !important;
line-height: 1.2 !important;
border-radius: var(--adys-r-pill) !important;
transition: all var(--adys-fast) var(--adys-ease);
max-height: 38px;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 500;
cursor: pointer;
}
.autoplay-toggle-btn .toggle-track {
position: relative;
width: 40px;
height: 22px;
border-radius: var(--adys-r-pill);
background: var(--adys-bg-hover);
border: 1px solid var(--adys-border);
transition: background var(--adys-med) var(--adys-ease), border-color var(--adys-med) var(--adys-ease);
flex-shrink: 0;
box-sizing: border-box;
}
.autoplay-toggle-btn .toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,.35);
transition: transform var(--adys-med) var(--adys-ease);
}
.autoplay-toggle-btn .toggle-label {
color: var(--adys-text-dim);
font-size: var(--adys-fs-sm);
transition: color var(--adys-fast) var(--adys-ease);
}
.autoplay-toggle-btn:not(.off) .toggle-track {
background: var(--adys-accent);
border-color: var(--adys-accent);
}
.autoplay-toggle-btn:not(.off) .toggle-thumb {
transform: translateX(18px);
}
.autoplay-toggle-btn:not(.off) .toggle-label {
color: var(--adys-accent-light);
}
.autoplay-toggle-btn:hover {
transform: scale(1.03);
}
@media (max-width: 640px) {
.search-modal-container {
max-height: 95vh !important;
width: 95% !important;
padding: 16px !important;
gap: 10px !important;
border-radius: 12px !important;
}
.search-modal-container .search-results-section {
min-height: 55vh !important;
}
.search-modal-container .search-tab-bar .search-tab {
font-size: 12px !important;
padding: 7px 4px !important;
min-height: 32px !important;
flex: 1 1 0 !important;
min-width: 0 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.search-modal-container .search-tab-bar {
gap: 4px !important;
padding: 2px 0 6px !important;
}
.search-modal-container .search-tab-bar .search-tab-count {
font-size: 11px !important;
}
}
@media screen and (min-width: 1025px) {
.pc-only {
display: block;
}
.mobile-only {
display: none;
}
.search-container {
display: none !important;
}
.liu-closePlayer{
position: fixed;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
line-height: 32px;
background-color: var(--adys-bg-deep);
display: flex;
}
.liu-closePlayer:hover{
background-color:#1f1f1f;
color:white;
transform: scale(1.1);
}
.playSpace {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr;
grid-column-gap:16px;
margin:8px 16px 16px;
height: auto;
gap:16px;
}
.player-panel .artplayer-app {
width: 100%;
height: 600px;
border-radius: 6px;
overflow: hidden;
}
.series {
height: 600px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.seletor-title {
border-radius: 6px 6px 0 0;
margin: 0;
flex-shrink: 0;
height: 60px;
line-height: 60px;
font-size:20px;
}
.history-btn {
font-size:20px !important;
}
.series .series-contianer {
flex: 1;
height: calc(600px - 60px);
margin-top: 0;
margin-bottom: 0;
padding:16px;
padding-bottom: 0;
border-radius: 0 0 6px 6px;
background-color: var(--adys-bg-deep);
grid-template-columns: repeat(4,1fr);
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
}
.control-panel {
margin: 8px 16px 16px;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.control-panel .control-btn-group {
margin-bottom: 0;
flex-shrink: 0;
}
.control-panel .pc-source-list {
flex: 1;
min-width: 0;
width: auto;
}
.control-panel .speed-test-btn {
max-width: 160px;
margin: 0;
}
}
@media screen and (max-width: 1024px) {
.liu-closePlayer {
display: none !important;
}
.playSpace{
grid-template-rows: auto 1fr;
grid-template-columns:1fr;
grid-row-gap:10px;
grid-column-gap:0px;
margin:0 16px;
}
.series-contianer{
grid-template-columns: repeat(auto-fill, minmax(66px, 1fr));
height: 160px;
padding:0 16px;
background-color: var(--adys-bg-deep);
border-radius: 0 0 6px 6px;
margin-top: 0;
}
.speed-test-btn {
max-width: 100%;
margin:16px auto !important;
display: block;
}
.series-tip-active .series-contianer {
animation: scrollTip 1.5s ease-in-out 1;
}
.artplayer-app{
height: calc(42vh);
min-height: 240px;
border-radius: 6px;
overflow: hidden;
}
.liu-playContainer {
padding-top: 15px;
padding-bottom: 90px;
}
.series {
margin-top:8px;
}
.title-right-actions {
gap: 8px;
}
/* 手机版隐藏连播按钮,只保留最近观看 */
.autoplay-toggle-btn {
display: none !important;
}
}
@media screen and (max-width: 768px) {
.series-contianer{
grid-template-columns: repeat(3,1fr);
height: 160px;
}
.artplayer-app{
height: 32vh;
min-height: 200px;
}
.mobile-only .sourceButtonList {
grid-template-columns: repeat(3, 1fr);
}
.mobile-btn-group {
flex-direction: column;
gap: 0;
}
}
@media screen and (max-width: 480px) {
.series-contianer{
grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
height: 150px;
}
.liu-btn{ font-size:16px; padding:10px 12px; }
.seletor-title{
font-size:16px;
height:45px;
line-height:45px;
}
.search-container { padding: 12px; }
.liu-playContainer {
padding-bottom: 90px;
padding-top: 10px;
}
}
@media screen and (max-width: 375px) {
.series-contianer{
grid-template-columns: repeat(2,1fr);
height: 140px;
grid-column-gap: 8px;
grid-row-gap: 8px;
}
/* 防止搜索切换栏被压缩 */
.search-modal-container .search-input-row,
.search-modal-container .search-history-section,
.search-modal-container .search-tab-bar {
flex-shrink: 0 !important;
}
.search-modal-container .search-results-section {
flex-shrink: 1 !important;
flex: 1 1 0 !important;
}
.series-selector{ font-size:14px; padding: 8px 4px; }
.artplayer-app{ height: 28vh; min-height: 180px; }
.mobile-only .sourceButtonList { grid-template-columns: repeat(3, 1fr); }
}
.seletor-title {
display: flex !important;
flex-direction: row !important;
justify-content: space-between !important;
}
.seletor-title .title-text {
order: 0 !important;
flex: 1 !important;
}
.seletor-title .title-right-actions {
order: 1 !important;
flex-shrink: 0 !important;
}
/* 片名放大 */
.seletor-title .title-text {
font-size: 22px !important; /* 根据需要调整,移动端可改为 18px */
font-weight: 600 !important;
letter-spacing: 0.5px;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.seletor-title .title-text {
color: #ffffff !important;
font-weight: 900 !important;
font-size: 24px !important;
text-shadow: 0 2px 10px rgba(0,0,0,0.9) !important;
letter-spacing: 0.5px;
}
}
@media screen and (max-width: 768px) {
.seletor-title .title-text {
font-size: 20px !important;
}
}
/* 搜索历史区域:固定高度,超出滚动 */
.search-modal-container .search-history-section {
max-height: 60px !important;
overflow-y: auto !important;
flex-wrap: nowrap !important;
gap: 4px 8px !important;
padding-bottom: 4px !important;
align-items: center !important;
}
.search-modal-container .search-history-tag {
white-space: nowrap !important;
flex-shrink: 0 !important;
}
/* 搜索结果区域:提高最小高度(移动端更高) */
.search-modal-container .search-results-section {
min-height: 35vh !important;
}
/* 底部关闭按钮区域缩短 */
.search-modal-container > div:last-child {
padding: 8px 20px !important;
}
.search-modal-container .settings-close-btn {
padding: 6px 24px !important;
font-size: 14px !important;
}
/* 移动端专用:结果区更高,关闭区更短 */
@media screen and (max-width: 768px) {
.search-modal-container .search-results-section {
min-height: 50vh !important;
}
.search-modal-container > div:last-child {
padding: 4px 16px !important;
}
}
`);
// ==================== 2. Vue 模板 ====================
const vueAppTemplate = `
{{vod_name}}选集
请不要相信视频中的广告!
暂无观看记录
{{ item.movieName }}
第{{ item.playHistory.playingIndex + 1 }}集
{{ formatHistoryTime(item.timestamp) }}
`;
// ==================== 3. 全局函数 ====================
// 历史时间格式化
function formatHistoryTime(timestamp) {
const diff = Date.now() - timestamp;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()}`;
}
// 全局历史浮窗
function showGlobalHistoryModal() {
const existingModal = document.getElementById('global-history-modal');
if (existingModal) {
existingModal.remove();
return;
}
const historyList = getUnifiedHistory();
const modalHtml = `
${historyList.length === 0 ? '
暂无观看记录
' :
historyList.map(item => {
const ph = item.playHistory || {};
const epInfo = ph.playingIndex !== undefined ? `
第${ph.playingIndex + 1}集` : '';
const pct = calcProgressPercent(ph);
const progressInfo = pct > 0
? `
`
: '';
const poster = item.posterUrl
? `
})
`
: `
`;
return `
${poster}
${escapeHtml(item.movieName)}
${epInfo}${progressInfo}${formatHistoryTime(item.timestamp)}
`}).join('')}
`;
const modalElement = htmlToElement(modalHtml);
if (!modalElement) return;
document.body.appendChild(modalElement);
const closeBtn = document.getElementById('global-history-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => modalElement.remove());
}
modalElement.addEventListener('click', (e) => {
if (e.target === modalElement) modalElement.remove();
});
modalElement.querySelectorAll('.history-item-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const movieId = btn.getAttribute('data-delete-id');
if (movieId && deleteUnifiedHistoryItem(movieId)) {
tip('已删除该记录及关联播放进度');
modalElement.remove();
showGlobalHistoryModal();
} else {
tip('删除失败');
}
});
});
const clearAllBtn = document.getElementById('global-history-clear-all');
if (clearAllBtn) {
clearAllBtn.addEventListener('click', () => {
if (confirm('确定要清空所有最近观看记录吗?这将同时清除所有影片的播放进度。')) {
clearAllUnifiedHistory();
modalElement.remove();
tip('已清空所有记录');
}
});
}
document.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', (e) => {
if (e.target.classList.contains('history-item-delete')) return;
const subjectUrl = item.getAttribute('data-subject-url');
const movieName = item.getAttribute('data-movie-name');
const poster = item.getAttribute('data-poster');
// 爱看影视类:站点自带播放器,跳转到其播放页
if (subjectUrl && (subjectUrl.indexOf('ikanbot') !== -1 || subjectUrl.indexOf('imoviebot') !== -1)) {
window.location.href = subjectUrl;
return;
}
if (movieName) {
modalElement.remove();
// 优先用历史条目存的播放源直接唤起播放器(零搜索)
const sourceJson = item.getAttribute('data-source');
if (sourceJson) {
try {
const sd = JSON.parse(sourceJson);
if (sd && sd.sources && sd.sources.length > 0) {
// uid 沿用原历史条目,保证直出后播放进度/选源不丢失(避免退回虚拟 uid 重新从第一集开始)
cmsPlayData = { title: sd.title || movieName, poster: sd.poster || poster || '', sources: sd.sources, uid: item.getAttribute('data-movie-id') || '' };
initVue();
return;
}
} catch (err) {}
}
// 旧条目无源:弹爱看搜索弹窗,自动搜索片名
openSearchModalWithKeyword(movieName);
}
});
});
}
// 搜索浮窗(仅爱看1/爱看2 双站)
function showSearchModal() {
const existing = document.querySelector('.search-modal-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'search-modal-overlay';
const container = document.createElement('div');
container.className = 'search-modal-container';
// 搜索输入行
const inputRow = document.createElement('div');
inputRow.className = 'search-input-row';
const input = document.createElement('input');
input.type = 'text';
input.className = 'search-input';
input.placeholder = '搜索爱看1 / 爱看2';
input.autofocus = true;
const button = document.createElement('button');
button.className = 'search-btn';
button.textContent = '搜索';
// 固定按钮宽度,防止文字变化抖动
button.style.minWidth = '80px';
// 标签切换栏
const tabBar = document.createElement('div');
tabBar.className = 'search-tab-bar';
tabBar.style.display = 'none';
// 创建两个爱看标签
const tabA1 = document.createElement('button');
tabA1.className = 'search-tab';
tabA1.innerHTML = '爱看1 ';
const tabA2 = document.createElement('button');
tabA2.className = 'search-tab';
tabA2.innerHTML = '爱看2 ';
// 将所有 tab 存入数组,便于统一控制
const allTabs = [tabA1, tabA2];
const tabMap = { a1: tabA1, a2: tabA2 };
// 标签外观完全由 CSS(含深/浅主题 !important 规则)控制,无需内联样式同步
// 将标签加入 tabBar(顺序:爱看1、爱看2)
tabBar.appendChild(tabA1);
tabBar.appendChild(tabA2);
// 搜索结果区域
const resultsSection = document.createElement('div');
resultsSection.className = 'search-results-section';
// 存储当前搜索结果
let allResults = { a1: [], a2: [] };
let currentTab = 'a1';
// 弹窗是否已关闭/已失效:结果返回后若已失效则不再渲染
let disposed = false;
// ---------- 更新标签可见性与激活状态 ----------
function updateTabVisibilityAndActive() {
let firstVisible = null;
for (const [key, tab] of Object.entries(tabMap)) {
const count = allResults[key] ? allResults[key].length : 0;
if (count === 0 && !tab.classList.contains('loading')) {
tab.style.display = 'none';
} else {
tab.style.display = '';
if (count > 0 && !firstVisible) firstVisible = key;
}
}
const activeTab = tabMap[currentTab];
const activeLoading = activeTab && activeTab.classList.contains('loading');
if (!activeLoading && activeTab && activeTab.style.display === 'none' && firstVisible) {
allTabs.forEach(t => t.classList.remove('active'));
tabMap[firstVisible].classList.add('active');
currentTab = firstVisible;
}
if (allResults[currentTab] && allResults[currentTab].length > 0) {
renderResults(allResults[currentTab]);
} else if (activeLoading) {
resultsSection.innerHTML = '';
} else if (firstVisible) {
renderResults(allResults[currentTab]);
} else {
renderResults([]);
}
tabBar.style.display = 'flex';
}
const renderResults = (list) => {
resultsSection.innerHTML = '';
if (!list || list.length === 0) {
resultsSection.innerHTML = '未找到相关结果
';
return;
}
list.forEach(r => {
const card = document.createElement('div');
card.className = 'search-result-card';
const posterHtml = r.poster
? `})
🎬
`
: `🎬
`;
const abstractHtml = r.abstract ? `${escapeHtml(r.abstract)}` : '';
const sourceLabel = escapeHtml(r.siteLabel || '爱看');
let extraHtml = '';
if (r.lineCount) {
extraHtml = `${escapeHtml(r.lineCount)}`;
}
card.innerHTML = `
${posterHtml}
${escapeHtml(r.title)}
${sourceLabel}
${extraHtml}
${abstractHtml}
`;
// 爱看结果:点击播放按钮或卡片任意区域 → 关闭弹窗并解析线路弹窗播放
const playAikanDirect = () => {
overlay.remove();
playAikanInPlayer(r);
};
card.addEventListener('click', (e) => {
const playBtn = (e.target && e.target.closest) ? e.target.closest('.result-play-btn') : null;
e.stopPropagation();
e.preventDefault();
addSearchHistory(input.value.trim());
renderSearchHistory();
if (playBtn) { e.stopPropagation(); e.preventDefault(); }
if (r.url) playAikanDirect();
});
resultsSection.appendChild(card);
});
};
const switchTab = (tab) => {
if (tabMap[tab].classList.contains('loading')) return;
currentTab = tab;
allTabs.forEach(t => t.classList.remove('active'));
tabMap[tab].classList.add('active');
const list = allResults[tab];
renderResults(list);
};
tabA1.addEventListener('click', () => switchTab('a1'));
tabA2.addEventListener('click', () => switchTab('a2'));
const performSearch = async () => {
const keyword = input.value.trim();
if (!keyword) { tip('请输入搜索关键词'); return; }
addSearchHistory(keyword);
renderSearchHistory();
button.disabled = true;
button.textContent = '搜索中...';
allResults = { a1: [], a2: [] };
currentTab = 'a1';
// 重置所有标签状态(显示)
allTabs.forEach(t => {
t.style.display = '';
t.classList.remove('loading', 'loaded-failed', 'active');
t.querySelector('.search-tab-count').textContent = '';
});
tabA1.classList.add('loading', 'active');
renderResults([]);
tabBar.style.display = 'flex';
historyContainer.style.display = 'none';
resultsSection.innerHTML = '';
let a1Done = false, a2Done = false;
// 标签搜索完成的统一处理
const handleTabDone = (tabKey, tabEl, results, setDone) => {
if (disposed) return;
allResults[tabKey] = results;
setDone();
tabEl.classList.remove('loading');
tabEl.querySelector('.search-tab-count').textContent = results.length > 0 ? `(${results.length})` : '';
if (results.length === 0) tabEl.classList.add('loaded-failed');
updateTabVisibilityAndActive();
};
// 并行搜索两个爱看站:爱看1 = www1.ikanbot.com,爱看2 = ikanbot.eu.org
const a1Promise = searchAikan(keyword, 'https://www1.ikanbot.com', '爱看1')
.then(r => handleTabDone('a1', tabA1, r, () => { a1Done = true; }))
.catch(() => handleTabDone('a1', tabA1, [], () => { a1Done = true; }));
const a2Promise = searchAikan(keyword, 'https://ikanbot.eu.org', '爱看2')
.then(r => handleTabDone('a2', tabA2, r, () => { a2Done = true; }))
.catch(() => handleTabDone('a2', tabA2, [], () => { a2Done = true; }));
Promise.allSettled([a1Promise, a2Promise]).then(() => {
__diag.info('爱看搜索完成: 关键词="' + keyword + '" 爱看1=' + allResults.a1.length + ' 爱看2=' + allResults.a2.length);
historyContainer.style.display = 'none';
button.disabled = false;
button.textContent = '搜索';
});
};
button.addEventListener('click', performSearch);
input.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); });
inputRow.appendChild(input);
inputRow.appendChild(button);
container.appendChild(inputRow);
// 搜索历史
const historyContainer = document.createElement('div');
historyContainer.id = 'search-history-container';
container.appendChild(historyContainer);
const renderSearchHistory = () => {
historyContainer.innerHTML = '';
const historyKeywords = getSearchHistory();
if (historyKeywords.length === 0) return;
const limited = historyKeywords.slice(0, 6);
const historySection = document.createElement('div');
historySection.className = 'search-history-section';
const label = document.createElement('span');
label.className = 'search-history-label';
label.textContent = '最近搜索';
historySection.appendChild(label);
limited.forEach(kw => {
const tag = document.createElement('span');
tag.className = 'search-history-tag';
tag.textContent = kw;
tag.addEventListener('click', () => { input.value = kw; performSearch(); });
historySection.appendChild(tag);
});
const clearBtn = document.createElement('button');
clearBtn.className = 'search-history-clear';
clearBtn.textContent = '清除';
clearBtn.addEventListener('click', () => {
const n = clearSearchRelatedData();
historyContainer.innerHTML = '';
tip(`已清除搜索历史及缓存(${n} 项)`);
});
historySection.appendChild(clearBtn);
historyContainer.appendChild(historySection);
};
renderSearchHistory();
container.appendChild(tabBar);
container.appendChild(resultsSection);
// 底部关闭按钮
const footerClose = document.createElement('div');
footerClose.style.cssText = 'padding: 12px 20px; border-top: 1px solid var(--adys-border); text-align: center; flex-shrink: 0;';
const closeBtnFooter = document.createElement('button');
closeBtnFooter.className = 'settings-close-btn';
closeBtnFooter.textContent = '关闭';
closeBtnFooter.style.cssText = 'background: var(--adys-accent); color: white; border: none; padding: 8px 32px; border-radius: 20px; font-size: 16px; cursor: pointer; transition: background 0.2s;';
closeBtnFooter.addEventListener('click', () => overlay.remove());
closeBtnFooter.addEventListener('mouseenter', () => closeBtnFooter.style.background = 'var(--adys-accent-hover)');
closeBtnFooter.addEventListener('mouseleave', () => closeBtnFooter.style.background = 'var(--adys-accent)');
footerClose.appendChild(closeBtnFooter);
container.appendChild(footerClose);
overlay.appendChild(container);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
// 弹窗被移除时标记失效,避免在途搜索结果继续渲染
const overlayObserver = new MutationObserver(() => {
if (!overlay.isConnected) {
disposed = true;
overlayObserver.disconnect();
}
});
overlayObserver.observe(document.body, { childList: true });
}
// 打开搜索弹窗并以指定片名自动搜索(腾讯点海报/详情页一键搜索入口)
function openSearchModalWithKeyword(keyword) {
const kw = (keyword || '').trim();
if (!kw) { showSearchModal(); return; }
showSearchModal();
setTimeout(() => {
const modalInput = document.querySelector('.search-modal-container .search-input');
if (modalInput) {
modalInput.value = kw;
const searchBtn = document.querySelector('.search-modal-container .search-btn');
if (searchBtn) searchBtn.click();
}
}, 100);
}
// ==================== 统一设置面板 ====================
function showSettingsPanel() {
const existing = document.getElementById('settings-panel-overlay');
if (existing) { existing.remove(); return; }
const autoPlayOn = GM_getValue(AUTO_NEXT_ENABLED_KEY, true);
const hgDetailOn = GM_getValue(HONGUO_DETAIL_KEY, false);
const currentTheme = getTheme();
const overlay = document.createElement('div');
overlay.id = 'settings-panel-overlay';
overlay.className = 'settings-modal-overlay';
overlay.innerHTML = `
红果详情页
关闭:点海报直接弹窗搜索;开启:点海报进详情页再一键搜索
`;
document.body.appendChild(overlay);
const closeModal = () => overlay.remove();
document.getElementById('settings-close-btn').addEventListener('click', closeModal);
const headerClose = document.getElementById('settings-header-close');
if (headerClose) headerClose.addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });
// 主题切换
const themeBtn = document.getElementById('settings-toggle-theme');
themeBtn.addEventListener('click', () => {
const newTheme = getTheme() === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
themeBtn.textContent = newTheme === 'dark' ? '🌙 深色' : '☀️ 浅色';
tip(`已切换到${newTheme === 'dark' ? '深色' : '浅色'}主题`);
});
// 自动连播开关
const autoplayToggle = document.getElementById('settings-toggle-autoplay');
autoplayToggle.addEventListener('click', () => {
const newVal = !GM_getValue(AUTO_NEXT_ENABLED_KEY, true);
GM_setValue(AUTO_NEXT_ENABLED_KEY, newVal);
autoplayToggle.classList.toggle('active', newVal);
tip(`自动连播已${newVal ? '开启' : '关闭'}`);
});
// 红果详情页开关
const hgDetailToggle = document.getElementById('settings-toggle-hgdetail');
hgDetailToggle.addEventListener('click', () => {
const newVal = !GM_getValue(HONGUO_DETAIL_KEY, false);
GM_setValue(HONGUO_DETAIL_KEY, newVal);
hgDetailToggle.classList.toggle('active', newVal);
tip(newVal ? '红果详情页已开启:点海报进详情页,由详情页一键搜索触发' : '红果详情页已关闭:点海报直接弹窗搜索片名');
});
// 清除观看历史
document.getElementById('settings-clear-history').addEventListener('click', () => {
if (confirm('确定要清除所有观看历史吗?这将同时删除所有影片的播放进度和记录。')) {
doClearHistory();
}
});
}
// 全局浮动按钮注入(站点感知)
function injectGlobalButtons() {
if (document.getElementById('global-buttons-container')) return;
const container = document.createElement('div');
container.id = 'global-buttons-container';
// 创建浮动菜单按钮(图标 + 文案标签,点击先收起菜单再执行动作)
const makeFabItem = (icon, label, onClick, active = false) => {
const btn = document.createElement('button');
btn.className = 'global-history-btn global-fab-item' + (active ? ' global-btn-active' : '');
btn.innerHTML = icon;
btn.title = label;
btn.setAttribute('aria-label', label);
btn.setAttribute('data-label', label);
btn.addEventListener('click', () => { collapseFab(); onClick(); });
return btn;
};
const historyBtn = makeFabItem('', '观看历史', showGlobalHistoryModal);
const searchBtn = makeFabItem('', '搜索爱看', showSearchModal);
const settingsBtn = makeFabItem('', '设置', showSettingsPanel);
// 根据站点添加导航按钮
const makeNavBtn = (text, label, urlOrFn, active = false) => {
const btn = document.createElement('button');
btn.className = 'global-history-btn global-fab-item' + (active ? ' global-btn-active' : '');
btn.innerHTML = text;
btn.title = label;
btn.setAttribute('aria-label', label);
btn.setAttribute('data-label', label);
btn.addEventListener('click', () => {
const url = typeof urlOrFn === 'function' ? urlOrFn() : urlOrFn;
window.location.href = url;
});
return btn;
};
const DUANJU_URL = 'https://hongguoduanju.com/category?sort_type=1';
const TENCENT_LIB_URL = 'https://v.qq.com/tv-series-list/v-index.html';
// 红果/爱看/腾讯页面:显示 历史、果、库、搜索、设置
container.appendChild(historyBtn);
container.appendChild(makeNavBtn('果', '红果短剧', DUANJU_URL, CURRENT_SITE === 'dj'));
container.appendChild(makeNavBtn('库', '腾讯片库', TENCENT_LIB_URL, CURRENT_SITE === 'tx'));
container.appendChild(searchBtn);
container.appendChild(settingsBtn);
// 移动端(或窄屏):折叠为绿色加号,点击展开菜单;电脑宽屏保持横向一排
const useMobileFab = isMobile || window.innerWidth <= 768;
if (useMobileFab) {
container.classList.add('mobile-fab-mode');
const fabMain = document.createElement('button');
fabMain.className = 'global-history-btn global-fab-main';
fabMain.innerHTML = '';
fabMain.title = '展开菜单';
fabMain.setAttribute('aria-label', '展开菜单');
container.appendChild(fabMain);
const updateFabPositions = () => {
const items = container.querySelectorAll('.global-fab-item');
const btnSize = 48;
const gap = 10;
items.forEach((item, i) => {
item.style.bottom = (btnSize + gap + i * (btnSize + gap)) + 'px';
});
};
const syncPlayButtons = (hide) => {
const mobileActionButtons = document.getElementById('mobile-action-buttons');
if (mobileActionButtons) {
mobileActionButtons.style.opacity = hide ? '0' : '1';
mobileActionButtons.style.pointerEvents = hide ? 'none' : 'auto';
mobileActionButtons.style.transition = 'opacity 0.2s ease';
}
};
fabMain.addEventListener('click', (e) => {
e.stopPropagation();
const isExpanded = container.classList.toggle('expanded');
if (isExpanded) {
updateFabPositions();
syncPlayButtons(true);
} else {
syncPlayButtons(false);
}
});
document.addEventListener('click', (e) => {
if (!container.contains(e.target) && container.classList.contains('expanded')) {
container.classList.remove('expanded');
syncPlayButtons(false);
}
});
}
document.body.appendChild(container);
}
// 红果分类栏「热榜」入口已废弃:顶栏原生已有"榜单",此入口冗余,不再注入并清除历史残留
function injectHongguoRankEntry() {
if (CURRENT_SITE !== 'dj') return;
const old = document.getElementById('hg-rank-entry');
if (old) old.remove();
}
// 红果分类/热榜列表页自动翻页:滚到底自动抓下一页并追加卡片
function initHongguoAutopager() {
if (CURRENT_SITE !== 'dj') return;
const path = location.pathname;
if (!/^\/(category|rank)/.test(path)) return;
if (typeof checkIsDetailPage === 'function' && checkIsDetailPage()) return;
// 同一路径不重复初始化;SPA 切分类 tab(真人剧/漫剧/AI剧/漫画)后路径变化 → 清理旧状态重初始化
if (window._hgAutopager && window._hgAutopagerPath === path) return;
if (typeof window._hgAutopagerCleanup === 'function') { try { window._hgAutopagerCleanup(); } catch (e) {} }
window._hgAutopager = true;
window._hgAutopagerPath = path;
const LINK_SEL = 'a[href*="/detail?series_id="], a[href^="/comic/"]';
const findNextLink = (rootDoc) => rootDoc.querySelector('a[aria-label="下一页"]');
const findListContainer = (rootDoc) => {
const links = rootDoc.querySelectorAll(LINK_SEL);
if (!links.length) return null;
let node = links[0];
let guard = 0;
while (node.parentElement && guard++ < 12) {
node = node.parentElement;
if (node.querySelectorAll(LINK_SEL).length >= Math.min(links.length, 5)) break;
}
return node;
};
let loading = false, stopped = false, nextUrl = (findNextLink(document) || {}).href || null;
// 底部状态条
const bar = document.createElement('div');
bar.style.cssText = 'padding:16px;text-align:center;color:#999;font-size:13px;';
bar.textContent = '下拉加载更多';
const loadNext = () => {
if (loading || stopped || !nextUrl) return;
loading = true;
bar.textContent = '加载中…';
GM_xmlhttpRequest({
method: 'GET',
url: nextUrl,
onload: (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText || '', 'text/html');
const curBox = findListContainer(document);
const nextBox = findListContainer(doc);
if (!curBox || !nextBox) { stopped = true; bar.textContent = '没有更多了'; loading = false; return; }
let added = 0;
Array.from(nextBox.children).forEach(child => {
const isCard = (child.matches && child.matches(LINK_SEL)) || (child.querySelector && child.querySelector(LINK_SEL));
if (isCard) {
// 新节点不会被网站自己的懒加载观察器接管,强制封面加载并去掉透明占位
child.querySelectorAll('img').forEach(img => {
img.setAttribute('loading', 'eager');
if (img.src) img.src = img.src;
});
child.querySelectorAll('[style*="opacity"]').forEach(el => {
if ((el.style.opacity || '') === '0') el.style.opacity = '1';
});
curBox.appendChild(child);
added++;
}
});
const nl = findNextLink(doc);
if (!added || !nl) { stopped = true; bar.textContent = '没有更多了'; }
else { nextUrl = nl.href; bar.textContent = '下拉加载更多'; }
} catch (e) { bar.textContent = '加载失败,上滑重试'; }
loading = false;
},
onerror: () => { loading = false; bar.textContent = '加载失败,上滑重试'; }
});
};
const onScroll = () => {
// SPA 切 tab 不刷新页面:路径变了(如从真人剧切到漫画)先重建当前列表的加载器,再由它接管滚动
if (location.pathname !== path) { initHongguoAutopager(); return; }
if (loading || stopped) return;
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 800) loadNext();
};
const cleanup = () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
if (bar && bar.parentNode) bar.parentNode.removeChild(bar);
};
window._hgAutopagerCleanup = cleanup;
const boot = () => {
if (!document.body) return;
document.body.appendChild(bar);
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
// 首屏若不足一屏,尝试自动加载一页
if (document.body.offsetHeight - window.innerHeight < 800) loadNext();
};
if (document.body) boot(); else document.addEventListener('DOMContentLoaded', boot);
}
// 红果列表页海报点击拦截:详情页开关关闭时,点海报不跳详情页,直接弹窗搜索海报片名
function initHongguoCardIntercept() {
if (CURRENT_SITE !== 'dj') return;
if (window._hgCardIntercept) return;
window._hgCardIntercept = true;
const DJ_CARD_SEL = 'a[href*="/detail?series_id="], a[href^="/comic/"], a[href*="/series/"]';
const SCRIPT_UI_SEL = '.search-modal-overlay, .settings-modal-overlay, .history-modal, #dgys2-player-root, .adys-btn-group, #mobile-action-buttons, #global-buttons-container';
// 片名提取:卡片名称节点 → img[alt] → title 属性(不用 innerText 兜底,避免误抓"更新至80集"等元数据)
const extractTitle = (card) => {
let title = '';
const nameEl = card.querySelector('[class*="name"], [class*="title"]');
if (nameEl) title = (nameEl.textContent || '').replace(/\s+/g, ' ').trim();
if (!title) {
const img = card.querySelector('img[alt]');
if (img && img.getAttribute('alt')) title = img.getAttribute('alt').trim();
}
if (!title && card.getAttribute('title')) title = card.getAttribute('title').trim();
return title;
};
const tryIntercept = (e) => {
// 详情页开关开启:放行原生跳转,由详情页"一键搜索"按钮触发搜索
if (GM_getValue(HONGUO_DETAIL_KEY, false)) return;
if (document.body.classList.contains('adys-playing')) return;
// 详情页内不拦截(相关推荐照常进详情页)
try { if (checkIsDetailPage()) return; } catch (err) {}
const t = e.target;
if (!t || !t.closest) return;
if (t.closest(SCRIPT_UI_SEL)) return;
const card = t.closest(DJ_CARD_SEL);
if (!card) return;
const title = extractTitle(card);
if (!title) return; // 取不到片名放行,避免卡死
e.preventDefault();
e.stopPropagation();
try { e.stopImmediatePropagation(); } catch (err) {}
try { openSearchModalWithKeyword(title); }
catch (err) { try { __diag.warn('红果卡片搜索异常: ' + err); } catch (_) {} }
};
// click 覆盖桌面与 React onClick;touchend 覆盖手机端提前导航
document.addEventListener('click', tryIntercept, true);
// 记录 touchstart 位置,touchend 时仅点按(位移很小)才拦截,避免列表滚动抬手误触
let hgTouchX = 0, hgTouchY = 0;
document.addEventListener('touchstart', (e) => {
const t = e.targetTouches && e.targetTouches[0];
if (t) { hgTouchX = t.clientX; hgTouchY = t.clientY; }
}, { capture: true, passive: true });
document.addEventListener('touchend', (e) => {
const t = e.changedTouches && e.changedTouches[0];
if (t) {
const dx = Math.abs(t.clientX - hgTouchX), dy = Math.abs(t.clientY - hgTouchY);
if (dx > 12 || dy > 12) return; // 视为滚动/滑动,不拦截
}
tryIntercept(e);
}, true);
}
// 收起扇形菜单的辅助函数
function collapseFab() {
const c = document.getElementById('global-buttons-container');
if (c) c.classList.remove('expanded');
// 恢复"一键搜索"按钮组
const mobileActionButtons = document.getElementById('mobile-action-buttons');
if (mobileActionButtons) {
mobileActionButtons.style.opacity = '1';
mobileActionButtons.style.pointerEvents = 'auto';
}
}
// ==================== 4. 标题解析 ====================
function parseTitle(name) {
if (!name) return { main: '', suffix: null };
let main = name;
let suffix = null;
const seasonMatch = name.match(/第(\d+)[季部]/);
if (seasonMatch) {
suffix = parseInt(seasonMatch[1], 10);
main = name.replace(/第\d+[季部]/, '').trim();
} else {
const numMatch = name.match(/\d+$/);
if (numMatch) {
suffix = parseInt(numMatch[0], 10);
main = name.replace(/\d+$/, '').trim();
}
}
return { main, suffix };
}
// ==================== 5. 主逻辑 initVue ====================
function initVue() {
const globalBtnContainer = document.getElementById('global-buttons-container');
if (globalBtnContainer) globalBtnContainer.style.display = 'none';
// 播放器打开:添加 body 类,CSS !important 隐藏手机浮动按钮(一键搜索/爱看影视)
document.body.classList.add('adys-playing');
// 如果有预加载的CMS搜索数据,构造虚拟 videoData
const hasCmsData = !!cmsPlayData;
if (hasCmsData) {
// 历史直出时沿用原历史条目的 uid(site_id),保证播放进度与选源跨会话连续;
// 普通 CMS 搜索直出无原始 uid,仍使用虚拟 uid dj_cms_{片名}
let uidSite = 'dj';
let uidId = 'cms_' + cmsPlayData.title;
if (cmsPlayData.uid) {
const sep = cmsPlayData.uid.indexOf('_');
if (sep > 0) { uidSite = cmsPlayData.uid.slice(0, sep); uidId = cmsPlayData.uid.slice(sep + 1); }
else uidId = cmsPlayData.uid;
}
videoDataBase = {
videoName: cmsPlayData.title,
videoYear: '',
videoFirstActor: '',
posterUrl: cmsPlayData.poster || '',
site: uidSite,
id: uidId,
subjectUrl: '',
};
}
const videoData = getVideoDataOnce();
if (!videoData || !videoData.videoName || videoData.videoName === '未知') {
__diag.warn('影片信息读取失败: ' + location.href);
tip(CURRENT_SITE === 'dj' ? '短剧信息读取失败,请刷新页面重试!' : '影片信息读取失败,请刷新页面重试!');
document.body.classList.remove('adys-playing');
resetPlayBtn();
return;
}
__diag.info('初始化播放器: 片名=' + videoData.videoName + ' 站点=' + videoData.site + ' uid=' + (videoData.site + '_' + videoData.id));
const oldApp = $('#dgys2-player-root');
if (oldApp) oldApp.remove();
const e = htmlToElement(vueAppTemplate);
if (!e) {
tip('界面生成失败,请刷新页面重试');
document.body.classList.remove('adys-playing');
resetPlayBtn();
return;
}
document.body.appendChild(e);
document.body.style.setProperty('overflow', 'hidden', 'important');
document.documentElement.style.setProperty('overflow', 'hidden', 'important');
new Vue({
el: '#dgys2-player-root',
data: {
art: {},
ok: false,
vod_name: videoData.videoName,
sources: [],
selectedSource: 0,
playingList: [],
playingIndex: 0,
isTestingSpeed: false,
isTipActive: false,
isScrollable: false,
searchKeyword: '',
retryCount: 0,
maxRetry: 1,
loadSuccessCount: 0,
loadTotalCount: 0,
loadFailedCount: 0,
wheelHandler: null,
speedTestTotal: 0,
speedTestCompleted: 0,
autoSwitchCount: 0,
maxAutoSwitch: 5,
failedSourceIndexes: [],
playTimer: null,
showHistoryModal: false,
historyList: [],
historyRecorded: false,
maxHistoryItems: MAX_HISTORY_ITEMS,
autoPlayNext: GM_getValue(AUTO_NEXT_ENABLED_KEY, true),
speedTestAbortController: null,
autoSpeedTestEnabled: false,
manualSpeedTestActive: false,
testingSourceSet: new Set(),
visibilityHandler: null,
},
computed: {
successSources() {
return this.sources
.map((source, idx) => ({ ...source, originalIndex: idx }))
.filter(s => s.status === 'success');
},
hasUntestedSources() {
return this.successSources.some(s => s.speed === -1);
}
},
watch: {
playingList() {
this.$nextTick(() => {
this.checkScrollable();
this.scrollToPlayingIndex();
});
},
playingIndex() {
this.$nextTick(() => this.scrollToPlayingIndex());
},
ok(newVal) {
if (newVal && isMobile) {
const mobileGroup = document.getElementById('mobile-action-buttons');
if (mobileGroup) mobileGroup.style.display = 'none';
}
}
},
methods: {
isSourceTesting(index) {
return this.testingSourceSet.has(index);
},
checkScrollable() {
const container = this.$refs.seriesContainer;
if (!container) return;
container.removeEventListener('scroll', this.checkScrollable);
this.isScrollable = container.scrollHeight > container.clientHeight;
container.addEventListener('scroll', this.checkScrollable);
},
// 手机端/PC 选集列表是固定高度的滚动容器,只显示可视区几集;
// 从历史恢复或切换集数后,把当前集所在行滚到容器顶部(置顶),
// 手机端空间小,置顶可最大化看到选中集及其后的集数
scrollToPlayingIndex() {
try {
const container = this.$refs.seriesContainer;
if (!container || !container.children || !container.children.length) return;
const el = container.querySelector('.series-selector.playing') || container.children[this.playingIndex];
if (!el) return;
let padTop = 0;
if (window.getComputedStyle) {
try { padTop = parseFloat(window.getComputedStyle(container).paddingTop) || 0; } catch (e) {}
}
const elTop = el.offsetTop - padTop - (container.clientTop || 0);
const elBottom = elTop + el.offsetHeight;
// 仅在当前集不在可视区内时置顶,避免每帧都跳动
if (elTop < container.scrollTop || elBottom > container.scrollTop + container.clientHeight) {
container.scrollTop = Math.max(0, elTop);
}
} catch (e) {}
},
handleSearch() {
const keyword = this.searchKeyword.trim();
if (!keyword) return;
showSearchModal();
// 将关键词填入弹窗输入框并自动触发搜索
setTimeout(() => {
const modalInput = document.querySelector('.search-modal-container .search-input');
if (modalInput) {
modalInput.value = keyword;
const searchBtn = document.querySelector('.search-modal-container .search-btn');
if (searchBtn) searchBtn.click();
}
}, 100);
},
toggleAutoPlayNext() {
this.autoPlayNext = !this.autoPlayNext;
GM_setValue(AUTO_NEXT_ENABLED_KEY, this.autoPlayNext);
tip(`自动连播已${this.autoPlayNext ? '开启' : '关闭'}`);
},
async startSpeedTest(keepExistingResults = false) {
if (this.isTestingSpeed) return;
const validSources = this.sources
.map((source, index) => ({ ...source, index }))
.filter(s => s.status === 'success' && s.playList.length > 0);
if (validSources.length === 0) return;
if (!keepExistingResults) {
validSources.forEach(s => { this.sources[s.index].speed = -1; });
}
// 测速候选过滤:命中会话缓存(10 分钟内)直接复用结果,不重新下载分片
const cacheNow = Date.now();
const toTest = validSources.filter(s => {
if (this.sources[s.index].speed !== -1) return false;
const cached = speedTestSessionCache.get(s.name);
if (cached && cacheNow - cached.time < SPEED_TEST_CACHE_TTL) {
this.sources[s.index].speed = cached.speed;
return false;
}
return true;
});
if (toTest.length === 0) {
this.autoSpeedTestEnabled = false;
return;
}
this.isTestingSpeed = true;
this.speedTestTotal = toTest.length;
this.speedTestCompleted = 0;
this.$forceUpdate();
this.speedTestAbortController = new AbortController();
const signal = this.speedTestAbortController.signal;
const groupSize = 3;
const sourceGroups = [];
for (let i = 0; i < toTest.length; i += groupSize) {
sourceGroups.push(toTest.slice(i, i + groupSize));
}
try {
for (const group of sourceGroups) {
if (signal.aborted) break;
await Promise.all(
group.map(async (source) => {
if (signal.aborted) return;
const index = source.index;
this.testingSourceSet.add(index);
this.$forceUpdate();
try {
const url = source.playList[this.playingIndex].url;
const result = await downloadtsList(url, signal);
if (signal.aborted) return;
if (!result.r || result.content.length === 0) {
// m3u8 拉不到或无 ts 分片:保持未测速(-1),不再伪造 0.1 MB/s
} else {
let tsList = result.content;
tsList = tsList.length > 8 ? tsList.slice(0, 8) : tsList;
let totalSize = 0;
const startTime = Date.now();
await Promise.all(
tsList.map(async (tsUrl) => {
if (signal.aborted) return;
const res = await get({ url: encodeURI(tsUrl), responseType: 'arraybuffer', timeout: 8000 }, 0, 8000, signal);
if (signal.aborted) return;
if (res.r) {
const size = res.content.byteLength ? res.content.byteLength / 1024 / 1024 : 0;
totalSize += size;
}
})
);
if (!signal.aborted) {
if (totalSize <= 0) {
// 8 秒内一个分片字节都没下成(被防盗链拦截/超时):保持未测速,不显示假 0.1
} else {
const duration = (Date.now() - startTime) / 1000;
let speed = duration > 0 ? totalSize / duration : 0.01;
speed = Math.max(Number(speed.toFixed(2)), 0.01);
this.sources[index].speed = speed;
}
}
}
} catch (e) {
// 异常:保持未测速,不伪造 0.1
} finally {
if (!signal.aborted) {
this.speedTestCompleted++;
this.testingSourceSet.delete(index);
this.$forceUpdate();
// 仅缓存真实测到速度的结果;未测速/失败不缓存,下次可重测
if (this.sources[index].speed > 0) {
speedTestSessionCache.set(source.name, { speed: this.sources[index].speed, time: Date.now() });
}
} else {
this.testingSourceSet.delete(index);
this.$forceUpdate();
}
}
})
);
}
} catch (e) {}
finally {
if (!signal.aborted) {
this.isTestingSpeed = false;
this.speedTestAbortController = null;
this.$forceUpdate();
if (this.hasUntestedSources === false) this.autoSpeedTestEnabled = false;
} else {
this.isTestingSpeed = false;
this.speedTestAbortController = null;
this.$forceUpdate();
}
}
},
stopSpeedTest() {
if (this.speedTestAbortController) {
this.speedTestAbortController.abort();
this.speedTestAbortController = null;
}
this.isTestingSpeed = false;
this.testingSourceSet.clear();
this.$forceUpdate();
},
async handleManualSpeedTest() {
if (this.isTestingSpeed) {
this.stopSpeedTest();
this.manualSpeedTestActive = false;
tip('测速已停止');
return;
}
if (this.successSources.length === 0) {
tip('无可用播放源,无法测速');
return;
}
if (this.art && !this.art.paused) {
this.art.pause();
}
this.manualSpeedTestActive = true;
try {
await this.startSpeedTest(true);
} finally {
this.manualSpeedTestActive = false;
}
if (!this.isTestingSpeed && this.hasUntestedSources === false) {
if (this.art && this.art.notice) { try { this.art.notice.show = '所有源测速完成'; } catch (e) {} }
}
},
autoSpeedTest() {
if (!this.autoSpeedTestEnabled) return;
if (this.isTestingSpeed) return;
if (!this.hasUntestedSources) {
this.autoSpeedTestEnabled = false;
return;
}
this.startSpeedTest(true);
},
sourceSelect(index) {
if (this.sources[index].status === 'failed') return;
const ct = this.art.currentTime;
this.selectedSource = index;
this.playingList = this.sources[index].playList;
// 新源集数可能少于当前播放集数:越界时回退到该源最后一集,避免播放空 URL
if (this.playingIndex >= this.playingList.length) {
this.playingIndex = Math.max(0, this.playingList.length - 1);
}
this.switchUrl(this.playingList[this.playingIndex].url);
this.art.once('video:canplay', () => { this.art.seek = ct; });
this.vod_name = this.sources[index].vod_name;
savePlayHistory({ selectedSource: index, selectedSourceName: this.sources[index].name });
this.retryCount = 0;
this.autoSwitchCount = 0;
},
playListSelect(index, autoPlay = false) {
this.playingIndex = index;
this.switchUrl(this.playingList[this.playingIndex].url);
savePlayHistory({ playingIndex: index, currentTime: 0, selectedSource: this.selectedSource, selectedSourceName: this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '' });
this.retryCount = 0;
if (autoPlay) {
setTimeout(() => {
if (this.art && this.art.play) {
this.art.play().catch(e => console.warn('自动播放失败', e));
}
}, 200);
}
},
saveCurrentPlayback() {
if (this.art && this.art.currentTime > 0) {
savePlayHistory({
currentTime: this.art.currentTime,
duration: this.art.duration || 0,
playbackRate: this.art.playbackRate,
volume: this.art.volume,
selectedSource: this.selectedSource,
selectedSourceName: this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '',
});
}
},
startPlayTimer() {
if (this.playTimer) clearInterval(this.playTimer);
// 进度写入 5s → 15s 降频(每次写入含 2 次存储读 + 1 次全历史数组写回)
this.playTimer = setInterval(() => {
this.saveCurrentPlayback();
}, 15000);
// 页面隐藏/关闭时立即落盘,补齐降频后的进度丢失窗口
if (!this.pageHideHandler) {
this.pageHideHandler = () => this.saveCurrentPlayback();
document.addEventListener('visibilitychange', this.pageHideHandler);
window.addEventListener('pagehide', this.pageHideHandler);
}
},
stopPlayTimerAndSave() {
if (this.playTimer) {
clearInterval(this.playTimer);
this.playTimer = null;
}
this.saveCurrentPlayback();
},
stopPlayTimer() {
if (this.playTimer) {
clearInterval(this.playTimer);
this.playTimer = null;
}
},
initArt(url) {
if (this.visibilityHandler) {
document.removeEventListener('visibilitychange', this.visibilityHandler);
this.visibilityHandler = null;
}
try {
const autoplay = false;
this.art = new Artplayer({
container: '.artplayer-app',
url: url,
autoplay: autoplay,
pip: true,
fullscreen: true,
fullscreenWeb: true,
autoMini: !isMobile,
screenshot: true,
hotkey: true,
airplay: true,
playbackRate: true,
setting: true,
miniProgressBar: !isMobile,
theme: 'var(--adys-accent-hover)',
moreVideoAttr: { crossOrigin: 'anonymous' },
controls: isMobile
? [{ name: 'resolution', html: '分辨率', position: 'right' }]
: [
{
name: 'prev-episode',
position: 'left',
html: '⏮',
tooltip: '上一集',
click: () => {
if (this.playingList.length > 1 && this.playingIndex > 0) {
this.playListSelect(this.playingIndex - 1, true);
} else {
if (this.art && this.art.notice) this.art.notice.show = '已经是第一集了';
}
},
},
{
name: 'next-episode',
position: 'left',
html: '⏭',
tooltip: '下一集',
click: () => {
if (this.playingList.length > 1 && this.playingIndex + 1 < this.playingList.length) {
this.playListSelect(this.playingIndex + 1, true);
} else {
if (this.art && this.art.notice) this.art.notice.show = '已经是最后一集了';
}
},
},
{ name: 'resolution', html: '分辨率', position: 'right' }
],
type: 'm3u8',
customType: { m3u8: playM3u8 },
});
this.visibilityHandler = () => {
if (document.hidden) {
if (this.art && !this.art.paused) {
this.art.pause();
}
}
};
document.addEventListener('visibilitychange', this.visibilityHandler);
this.art.on('video:play', () => this.startPlayTimer());
this.art.on('video:pause', () => this.stopPlayTimerAndSave());
this.art.on('video:ended', () => this.stopPlayTimer());
// 网页全屏/原生全屏时隐藏底部搜索栏,防止遮挡播放器控制栏
this.art.on('fullscreenWeb', (state) => {
if (state) document.body.classList.add('adys-fullscreen-web');
else document.body.classList.remove('adys-fullscreen-web');
});
this.art.on('fullscreen', (state) => {
if (state) document.body.classList.add('adys-fullscreen-web');
else document.body.classList.remove('adys-fullscreen-web');
});
this.art.on('error', () => this.handlePlayError());
this.art.on('video:stalled', () => {
setTimeout(() => {
if (this.art.video.readyState < 3) this.handlePlayError();
}, 5000);
});
this.art.on('video:canplay', () => { this.retryCount = 0; });
let seekRetryCount = 0;
const trySeek = (targetTime) => {
if (this.art.video.readyState >= 1 && this.art.video.duration && !isNaN(this.art.video.duration)) {
this.art.seek = targetTime;
seekRetryCount = 0;
} else if (seekRetryCount < 5) {
seekRetryCount++;
setTimeout(() => trySeek(targetTime), 500);
}
};
this.art.on('video:loadedmetadata', () => {
this.art.controls.resolution.innerText = this.art.video.videoHeight + 'P';
const history = getPlayHistory();
if (history.currentTime && history.currentTime > 0) trySeek(history.currentTime);
if (history.playbackRate) this.art.playbackRate = history.playbackRate;
if (history.volume !== undefined) this.art.volume = history.volume;
});
this.art.on('video:ended', () => {
if (!this.autoPlayNext) return;
if (this.playingList.length > 1 && this.playingIndex + 1 < this.playingList.length) {
this.playListSelect(this.playingIndex + 1, true);
}
});
if (!isMobile) {
this.art.on('video:play', () => { if (this.isTestingSpeed && !this.manualSpeedTestActive) this.stopSpeedTest(); });
this.art.on('video:pause', () => { if (this.hasUntestedSources && !this.isTestingSpeed) this.autoSpeedTest(); });
this.art.on('video:ended', () => { if (this.hasUntestedSources && !this.isTestingSpeed) this.autoSpeedTest(); });
} else {
this.art.on('video:play', () => { if (this.isTestingSpeed && !this.manualSpeedTestActive) this.stopSpeedTest(); });
}
this.art.on('destroy', () => {
if (!document.hidden && this.art && !this.art.paused && this.art.currentTime > 0) {
savePlayHistory({ currentTime: this.art.currentTime });
}
this.stopPlayTimer();
this.stopSpeedTest();
if (this.visibilityHandler) {
document.removeEventListener('visibilitychange', this.visibilityHandler);
this.visibilityHandler = null;
}
});
} catch (e) {
tip('播放器初始化失败,请刷新页面重试');
}
},
handlePlayError() {
__diag.warn('播放出错: 源=' + (this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : this.selectedSource) + ' 第' + (this.playingIndex + 1) + '集 重试计数=' + this.retryCount);
if (this.retryCount < this.maxRetry) {
this.retryCount++;
this.art.notice.show = `播放失败,第${this.retryCount}次重试...`;
this.switchUrl(this.playingList[this.playingIndex].url);
} else {
this.retryCount = 0;
if (!this.failedSourceIndexes.includes(this.selectedSource)) this.failedSourceIndexes.push(this.selectedSource);
if (this.autoSwitchCount >= this.maxAutoSwitch) {
__diag.warn('自动换源次数已达上限');
this.art.notice.show = '自动换源次数已达上限,请手动切换源';
tip('自动换源次数已达上限,请手动切换源');
return;
}
const validSources = this.sources
.map((s, idx) => ({ ...s, index: idx }))
.filter(s => s.status === 'success' && !this.failedSourceIndexes.includes(s.index));
if (validSources.length > 0) {
const nextSource = validSources[0];
this.autoSwitchCount++;
__diag.info('自动换源: ' + (this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '?') + ' -> ' + nextSource.name);
this.art.notice.show = `当前源播放失败,自动切换到${nextSource.name}`;
this.sourceSelect(nextSource.index);
} else {
__diag.error('所有源均播放失败');
this.art.notice.show = '所有源均播放失败,请刷新页面重试';
tip('所有源均播放失败,请刷新页面重试');
}
}
},
switchUrl(url) {
try {
if (!this.art) return;
// 先销毁旧 Hls 实例:保证「同 URL 失败重试」时真正重建播放器,而不是只换 src 不重载
if (this.art.hls) {
try { this.art.hls.destroy(); } catch (e1) {}
this.art.hls = null;
}
// 切换交给 Artplayer(m3u8 走 customType 重建 Hls),不再手工直写 video.src,
// 避免原生播放器直接解析 m3u8 导致黑屏或二次初始化。
this.art.switchUrl(url);
} catch (e) {
__diag.warn('switchUrl 异常: ' + (e && e.message ? e.message : e));
}
},
closePlayer() {
try {
__diag.info('播放器已关闭: ' + (this.vod_name || ''));
hideLoadingTip();
this.stopSpeedTest();
this.stopPlayTimer();
if (this.art && this.art.destroy) this.art.destroy();
if (this.$destroy) this.$destroy();
if (this.visibilityHandler) {
document.removeEventListener('visibilitychange', this.visibilityHandler);
this.visibilityHandler = null;
}
if (this.pageHideHandler) {
document.removeEventListener('visibilitychange', this.pageHideHandler);
window.removeEventListener('pagehide', this.pageHideHandler);
this.pageHideHandler = null;
}
const container = this.$refs.seriesContainer;
if (container) container.removeEventListener('scroll', this.checkScrollable);
if (this.wheelHandler) {
const sourceList = this.$el && this.$el.querySelector('.pc-source-list');
if (sourceList) sourceList.removeEventListener('wheel', this.wheelHandler);
this.wheelHandler = null;
}
const app = $('#dgys2-player-root');
if (app) app.remove();
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
// 重置视频数据缓存,确保下次播放使用新数据
videoDataBase = null;
const globalBtnContainer = document.getElementById('global-buttons-container');
if (globalBtnContainer) globalBtnContainer.style.display = 'flex';
// 播放器关闭:移除 body 类,恢复手机浮动按钮
document.body.classList.remove('adys-playing');
document.body.classList.remove('adys-fullscreen-web');
resetPlayBtn();
} catch (e) {}
},
openHistoryModal() { this.loadHistoryList(); this.showHistoryModal = true; },
closeHistoryModal() { this.showHistoryModal = false; },
loadHistoryList() { this.historyList = getUnifiedHistory().slice(0, MAX_HISTORY_ITEMS); },
getHistoryProgressPercent(item) {
return calcProgressPercent(item.playHistory);
},
formatHistoryTime,
jumpToHistory(item) {
if (item.subjectUrl && (item.subjectUrl.indexOf('ikanbot') !== -1 || item.subjectUrl.indexOf('imoviebot') !== -1)) {
// 爱看影视:跳转到爱看站播放页(爱看站自带播放器,脚本不解析其源)
window.location.href = item.subjectUrl;
return;
}
if (item.movieName) {
this.closeHistoryModal();
this.closePlayer();
// 优先用历史条目里存的播放源直接唤起播放器(零搜索);旧条目无源时回退缓存直出/资源站搜索
if (item.sourceData && item.sourceData.sources && item.sourceData.sources.length > 0) {
// uid 沿用原历史条目,保证直出后播放进度/选源跨会话连续
cmsPlayData = { title: item.sourceData.title || item.movieName, poster: item.sourceData.poster || item.posterUrl || '', sources: item.sourceData.sources, uid: item.movieId || '' };
initVue();
return;
}
// 旧条目无源:弹爱看搜索弹窗,自动搜索片名
openSearchModalWithKeyword(item.movieName);
return;
}
this.closeHistoryModal();
},
deleteHistoryItem(movieId) { if (deleteUnifiedHistoryItem(movieId)) { this.loadHistoryList(); tip('已删除该记录及关联播放进度'); } else tip('删除失败,请重试'); },
confirmClearAllHistory() { if (confirm('确定要清空所有最近观看记录吗?这将同时清除所有影片的播放进度。')) { clearAllUnifiedHistory(); this.loadHistoryList(); tip('已清空所有记录'); } },
recordCurrentToHistory() {
if (this.historyRecorded) return;
const videoData = getVideoDataOnce();
if (!videoData) return;
const uid = getUid();
// 用当前已加载的播放源构造 sourceData(cmsPlayData 在搜索直出分支用完后已被置空,
// 直接引用它会导致历史条目永远存不到源)。历史点击时直接从源唤起播放器(零搜索),
// 与搜索弹窗直出播放的体验一致。
const loadedSources = this.sources
.filter(s => s.status === 'success' && s.playList && s.playList.length > 0)
.map(s => ({ name: s.name, playList: s.playList, vod_name: s.vod_name || '' }));
const sourceData = loadedSources.length > 0
? { title: videoData.videoName, poster: videoData.posterUrl || '', sources: loadedSources }
: null;
addToUnifiedHistory(uid, videoData.videoName, videoData.subjectUrl, getPlayHistory(), videoData.posterUrl, sourceData);
this.historyRecorded = true;
}
},
async created() {
try {
let tempSources = [];
// 有预加载数据(爱看搜索结果直出)时,跳过搜索直接使用
if (hasCmsData && cmsPlayData.sources && cmsPlayData.sources.length > 0) {
this.loadTotalCount = cmsPlayData.sources.length;
this.loadSuccessCount = cmsPlayData.sources.length;
tempSources = cmsPlayData.sources.map(s => ({
name: s.name,
playList: s.playList,
vod_name: s.vod_name,
speed: -1,
status: 'success',
errorMsg: '搜索结果直出',
}));
cmsPlayData = null; // 用完即清,避免下次复用
} else {
// 纯净版:无预加载数据(未经过爱看搜索)时不发起任何资源站搜索,直接提示并关闭
hideLoadingTip();
tip('未找到可播放数据,请先搜索爱看');
this.closePlayer();
return;
}
hideLoadingTip();
// 排序:集数从多到少(爱看直出源均为成功源)
tempSources.sort((a, b) => {
return b.playList.length - a.playList.length;
});
this.sources = tempSources;
__diag.info('资源搜索完成: 成功 ' + this.loadSuccessCount + ' / 失败 ' + this.loadFailedCount + ' / 共 ' + this.loadTotalCount + ' (片名=' + videoData.videoName + ')');
const history = getPlayHistory();
let targetSourceIndex = -1;
if (history.selectedSourceName) {
const nameMatchIndex = this.sources.findIndex(s => s.name === history.selectedSourceName && s.status === 'success');
if (nameMatchIndex !== -1) targetSourceIndex = nameMatchIndex;
}
if (targetSourceIndex === -1 && history.selectedSource !== undefined && this.sources[history.selectedSource] && this.sources[history.selectedSource].status === 'success') targetSourceIndex = history.selectedSource;
// 无历史记录时默认选中第一个(即集数最多的源)
if (targetSourceIndex === -1) targetSourceIndex = this.sources.findIndex(s => s.status === 'success');
if (targetSourceIndex !== -1) {
this.selectedSource = targetSourceIndex;
this.playingList = this.sources[targetSourceIndex].playList;
this.vod_name = this.sources[targetSourceIndex].vod_name;
// 保存选中的源到播放历史,确保下次打开时恢复同一个源
savePlayHistory({ selectedSource: targetSourceIndex, selectedSourceName: this.sources[targetSourceIndex].name });
let targetPlayingIndex = 0;
if (history.playingIndex !== undefined && history.playingIndex >= 0 && history.playingIndex < this.playingList.length) targetPlayingIndex = history.playingIndex;
this.playingIndex = targetPlayingIndex;
this.ok = true;
await this.$nextTick();
this.checkScrollable();
if (isMobile && this.isScrollable) {
const uid = getUid();
const key = `tip_shown_${uid}`;
const alreadyShown = GM_getValue(key, false);
if (!alreadyShown) {
this.isTipActive = true;
GM_setValue(key, true);
setTimeout(() => { this.isTipActive = false; }, 1500);
}
}
this.initArt(this.playingList[this.playingIndex].url);
if (!isMobile) {
this.autoSpeedTestEnabled = true;
setTimeout(() => { if (this.autoSpeedTestEnabled && this.hasUntestedSources) this.startSpeedTest(true); }, 500);
}
// 海报:已有则不重复获取;没有时优先复用统一历史里已存的同名片海报,
// 实在没有才用首个成功源的封面补全,避免每次开播都重新抓海报
try {
const vdNow = getVideoDataOnce();
if (vdNow && !vdNow.posterUrl) {
const curName = vdNow.videoName || '';
const hist = getUnifiedHistory();
const hit = hist.find(it => it && matchTitle(it.movieName || '', curName) && it.posterUrl);
if (hit && hit.posterUrl) vdNow.posterUrl = hit.posterUrl;
else {
const picSource = this.sources.find(s => s.status === 'success' && s.vod_pic);
if (picSource) vdNow.posterUrl = picSource.vod_pic;
}
}
} catch (e) {}
this.recordCurrentToHistory();
} else {
tip(CURRENT_SITE === 'dj' ? '未搜索到任何可用短剧资源,可使用底部搜索框查找对应短剧' : '未搜索到任何可用资源,可使用底部搜索框查找对应影片');
this.closePlayer();
}
} catch (e) {
__diag.error('播放器 created 异常: ' + (e && e.stack ? e.stack : (e && e.message)));
hideLoadingTip();
tip('界面加载失败,请刷新页面重试');
this.closePlayer();
} finally {
resetPlayBtn();
}
},
mounted() {
if (!isMobile) {
this.$nextTick(() => {
const sourceList = this.$el.querySelector('.pc-source-list');
if (sourceList && !this.wheelHandler) {
this.wheelHandler = (e) => {
if (sourceList.scrollWidth > sourceList.clientWidth) {
e.preventDefault();
sourceList.scrollLeft += e.deltaY;
}
};
sourceList.addEventListener('wheel', this.wheelHandler, { passive: false });
}
});
}
},
beforeDestroy() {
if (this.wheelHandler) {
const sourceList = this.$el && this.$el.querySelector('.pc-source-list');
if (sourceList) sourceList.removeEventListener('wheel', this.wheelHandler);
this.wheelHandler = null;
}
this.stopPlayTimer();
this.stopSpeedTest();
if (this.visibilityHandler) {
document.removeEventListener('visibilitychange', this.visibilityHandler);
this.visibilityHandler = null;
}
}
});
}
// ==================== 6. 播放按钮注入(站点感知)====================
// 创建按钮对的辅助函数(避免重复代码)
function createPlayButtonPair() {
const playBtn = htmlToElement(``);
window.doubanPlayBtn = playBtn;
playBtn.onclick = () => {
playBtn.innerText = '搜索中...';
playBtn.style.backgroundColor = '#9e9e9e';
playBtn.style.color = '#1a1a1a';
playBtn.disabled = true;
playBtn.style.cursor = 'not-allowed';
const vd = getVideoDataOnce();
const name = (vd && vd.videoName && vd.videoName !== '未知') ? vd.videoName : '';
openSearchModalWithKeyword(name);
setTimeout(() => { resetBtnEl(playBtn); }, 300);
};
return { playBtn };
}
// 红果标题挂载点:新版详情页 h1 为 -webkit-box + line-clamp + overflow:hidden,
// 按钮注入 h1 内部会被 line-clamp 裁剪或同行挤压 → 注入其父容器(新版父容器为纵向 flex)
function getDjTitleEl() {
const h1 = document.querySelector('h1, .drama-title, .title, [class*="pc-title-"], [class*="m-title-"]');
if (!h1) return null;
try {
const cs = getComputedStyle(h1);
if ((cs.display === '-webkit-box' || cs.overflow === 'hidden') && h1.parentElement) {
return h1.parentElement;
}
} catch (e) {}
return h1;
}
// 电脑版红果详情:把"一键搜索"塞进原生"播放正片"按钮的位置,隐藏原生按钮
function injectDjPcPlay() {
const native = document.querySelector('a[class*="pc-hero-btn-play-"]');
if (!native || !native.parentElement) return false;
const holder = native.parentElement;
if (holder.querySelector('.play-btn')) return true;
const { playBtn } = createPlayButtonPair();
native.style.display = 'none';
holder.insertBefore(playBtn, native);
return true;
}
function hijackRedFruitAutoplay() {
if (isMobile || CURRENT_SITE !== 'dj') return;
if (window._adysHijackAutoplay) return;
window._adysHijackAutoplay = true;
let armed = true;
const disarm = () => { armed = false; };
document.addEventListener('click', disarm, { once: true, capture: true });
document.addEventListener('keydown', disarm, { once: true, capture: true });
document.addEventListener('touchstart', disarm, { once: true, capture: true });
const isScriptVideo = (video) =>
!!(video.closest && video.closest('.artplayer-app, .art-video-player, .adys-player'));
const hijackOne = (video) => {
if (!video || video.dataset.adysHijacked) return;
if (isScriptVideo(video)) return; // 不拦截脚本自己的播放器
video.dataset.adysHijacked = '1';
// capture 阶段先于页面 JS 的 play 处理:play 事件可 cancelable,preventDefault 可取消播放
video.addEventListener('play', (e) => {
if (armed) {
try { e.preventDefault(); } catch (err) {}
try { video.pause(); } catch (err) {}
}
}, true);
// 脚本后加载、视频已出现/已播放的情况:立即暂停
try { video.pause(); } catch (err) {}
};
const scan = () => {
document.querySelectorAll('video').forEach(hijackOne);
};
scan();
observeDebounced(document.documentElement, scan, { delay: 100 });
}
// MutationObserver:React SPA 重新渲染后自动重新注入按钮
function setupPlayButtonObserver() {
if (window.playButtonObserver) {
window.playButtonObserver.disconnect();
}
let reinjectTimer = null;
let reinjectCount = 0;
const MAX_REINJECTS = 50;
window.playButtonObserver = new MutationObserver(() => {
if (reinjectTimer) return;
reinjectTimer = setTimeout(() => {
reinjectTimer = null;
if (reinjectCount >= MAX_REINJECTS) return;
// 电脑版红果:优先顶回"播放正片"原位;React 重渲染后原生按钮会恢复,重新隐藏并注入
if (CURRENT_SITE === 'dj' && injectDjPcPlay()) { reinjectCount = 0; return; }
const targetEl = CURRENT_SITE === 'dj'
? getDjTitleEl()
: $('h1');
if (!targetEl) return;
// 按钮稳定存在 → 复位计数。避免 SPA 频繁重渲染导致 reinjectCount 累积到 MAX_REINJECTS 后永久停摆
if (targetEl.querySelector('.play-btn')) { reinjectCount = 0; return; }
// 按钮被 React 移除,重新注入(仅一键搜索)
const { playBtn } = createPlayButtonPair();
const btnWrapper = document.createElement('div');
btnWrapper.className = 'adys-btn-group';
btnWrapper.appendChild(playBtn);
targetEl.appendChild(btnWrapper);
reinjectCount++;
}, 300);
});
window.playButtonObserver.observe(document.body, { childList: true, subtree: true });
}
// 移动端 SPA 路由监听:检测 URL 变化后重新注入按钮
let lastCheckedUrl = location.href;
let mobileBtnCheckTimer = null;
function setupMobileRouteWatcher() {
if (window._adouMobileRouteWatcher) return;
window._adouMobileRouteWatcher = true;
// MutationObserver:DOM 变化时检查按钮是否存在
if (window.playButtonObserver) {
window.playButtonObserver.disconnect();
}
window.playButtonObserver = new MutationObserver(() => {
if (mobileBtnCheckTimer) return;
mobileBtnCheckTimer = setTimeout(() => {
mobileBtnCheckTimer = null;
// URL 变化时重新检测详情页状态
if (location.href !== lastCheckedUrl) {
lastCheckedUrl = location.href;
videoDataBase = null; // URL 变化后重置视频数据缓存
injectPlayButton();
return;
}
// 按钮被 SPA 移除时重新注入
if (isMobile) {
const group = document.getElementById('mobile-action-buttons');
if (!group && checkIsDetailPage()) {
injectPlayButton();
}
}
// 回到详情页时重新拉起 URL 轮询(轮询可能已因离开详情页而停止)
ensureMobileUrlPoll();
}, 300);
});
window.playButtonObserver.observe(document.body, { childList: true, subtree: true });
// 定期轮询 URL 变化(SPA pushState/replaceState 不触发 popstate)
ensureMobileUrlPoll();
}
// 移动端 URL 轮询:自管理生命周期,离开详情页即停止,回到详情页由观察器重新拉起
let mobileUrlPollTimer = null;
function ensureMobileUrlPoll() {
if (mobileUrlPollTimer) return;
if (!checkIsDetailPage()) return;
// 仅 SPA 站(红果/爱看)需要轮询:URL 会随站内导航变化
if (CURRENT_SITE === 'db') return;
mobileUrlPollTimer = setInterval(() => {
if (location.href !== lastCheckedUrl) {
lastCheckedUrl = location.href;
videoDataBase = null; // URL 变化后重置视频数据缓存,确保读取新页面数据
if (!checkIsDetailPage()) {
// 已离开详情页:停止常驻轮询,避免 1s 定时器永久运行
clearInterval(mobileUrlPollTimer);
mobileUrlPollTimer = null;
return;
}
injectPlayButton();
injectGlobalButtons(); // SPA 导航后也重新检查全局按钮
}
}, 1000);
}
// 详情页检测(动态读取当前 URL,不依赖脚本启动时的缓存)
function checkIsDetailPage() {
const currentHref = location.href;
const currentPath = location.pathname;
if (CURRENT_SITE === 'dj') {
// 红果详情页 URL 格式(含新版 /player/xxx):
// 1. /detail?series_id=xxx
// 2. /series/xxx(slug+数字ID)
// 3. /player/<数字ID>(新版详情/播放页)
return (/\/detail/.test(currentPath) && /series_id=/.test(currentHref)) ||
/\/series\/.+/.test(currentPath) ||
/\/player\/\d+/.test(currentPath);
} else if (CURRENT_SITE === 'db') {
return /\/subject\/\d+/.test(currentHref) ||
(location.hostname === 'm.douban.com' && currentPath.startsWith('/movie/subject/')) ||
(location.hostname === 'www.douban.com' && currentPath === '/doubanapp/dispatch' && currentHref.includes('/subject/'));
}
return false;
}
function injectPlayButton() {
if (!checkIsDetailPage()) return;
// 电脑版红果详情页:默认不播放页面原生视频(避免与脚本播放器双音)
hijackRedFruitAutoplay();
try {
if (isMobile) {
// 手机版:浮动按钮组
let btnGroup = document.getElementById('mobile-action-buttons');
if (!btnGroup) {
btnGroup = document.createElement('div');
btnGroup.id = 'mobile-action-buttons';
btnGroup.className = 'mobile-action-buttons';
document.body.appendChild(btnGroup);
}
if (!btnGroup.querySelector('.play-btn')) {
const { playBtn } = createPlayButtonPair();
btnGroup.appendChild(playBtn);
}
setupMobileRouteWatcher();
} else {
// 电脑版:优先顶替原生"播放正片"按钮位置;找不到再退到标题旁
const injectPc = () => {
if (CURRENT_SITE === 'dj' && injectDjPcPlay()) { setupPlayButtonObserver(); return true; }
const targetEl = CURRENT_SITE === 'dj'
? getDjTitleEl()
: $('h1');
if (!targetEl) return false;
if (!targetEl.querySelector('.play-btn')) {
const { playBtn } = createPlayButtonPair();
const btnWrapper = document.createElement('div');
btnWrapper.className = 'adys-btn-group';
btnWrapper.appendChild(playBtn);
targetEl.appendChild(btnWrapper);
setupPlayButtonObserver();
}
return true;
};
if (!injectPc()) setTimeout(injectPc, 1000);
}
} catch (e) {
tip('播放按钮加载失败,请刷新页面重试');
}
}
// ==================== 8. 菜单命令 ====================
GM_registerMenuCommand('📤 下载诊断日志', () => {
const ok = __diag.download();
if (ok) { try { tip('诊断日志已下载,请将文件发送给开发者排查'); } catch (e) {} }
});
GM_registerMenuCommand('清除观看历史', () => {
if (confirm('确定要清除所有观看历史吗?这将同时删除所有影片的播放进度和记录。')) {
doClearHistory();
}
});
{
let _autoPlayNextEnabled = GM_getValue(AUTO_NEXT_ENABLED_KEY, true);
GM_registerMenuCommand(`${_autoPlayNextEnabled ? '✅' : '❌'} 自动连播`, () => {
_autoPlayNextEnabled = !_autoPlayNextEnabled;
GM_setValue(AUTO_NEXT_ENABLED_KEY, _autoPlayNextEnabled);
tip(`自动连播已${_autoPlayNextEnabled ? '开启' : '关闭'}(仅对多集剧集生效)`);
});
}
// ==================== 8b. 腾讯片库频道整合(移植自 tx-unified-list) ====================
function initTencentLibrary() {
if (CURRENT_SITE !== 'tx') return;
// ---------- 频道配置 ----------
const TX_CHANNELS = [
{ id: '100173', name: '电影', params: 'sort=75' },
{ id: '100113', name: '电视剧', params: 'sort=75' },
{ id: '100109', name: '综艺', params: 'sort=75' },
{ id: '100119', name: '动漫', params: 'sort=75' },
{ id: '100150', name: '少儿', params: 'itype=75' },
{ id: '120188', name: '短剧', params: 'attraction=1' },
{ id: '100105', name: '纪录片', params: 'sort=75' },
{ id: '100103', name: '体育', params: 'irace=11' },
];
const TX_BASE = 'https://v.qq.com/tv-series-list/v-index.html';
function txChannelUrl(c) {
return TX_BASE + '?filter_params=' + c.params +
'&page_id=channel_list_second_page&channel_id=' + c.id;
}
function txCurrentChannelId() {
try { return new URL(location.href).searchParams.get('channel_id') || ''; }
catch (e) { return ''; }
}
const txState = { activeTab: null };
// ---------- 样式 ----------
const TX_CSS = `
/* ---------- 自然滚动布局(不锁高度) ---------- */
html, body {
margin: 0 !important; padding: 0 !important;
width: 100% !important;
background: #fff !important;
}
*, *::before, *::after { box-sizing: border-box !important; }
/* ---------- 隐藏腾讯外壳 ---------- */
.top-bar-wrap, .left-bar-wrap, .v-left-nav-area, .top-nav-wrap,
.search-wrap, #ssi-user-bar, .back-button, .tag-bar,
#login_win, .web-login-container, #toast-container,
.gatherable-layer, [class*="gatherable"] {
display: none !important;
}
/* ---------- 底部遮挡清除 ---------- */
.screen-list__footer, .screen-list__loading, .lazy-list__footer, .lazy-list__loading,
.filter-expand, .quick_pop_footer,
.app-download-bar, .app-download, .download-bar,
.bottom-guide, .guide-bottom, .v-bottom-bar, .bottom-bar-wrap,
.mobile-tab-bar, .screen-app-download,
[class*="bottom-banner"], [class*="app-promote"],
[class*="download-app"], [class*="footer-fixed"],
[class*="fixed-bottom"], [class*="bottom-fixed"],
[class*="guide-bar"], [class*="openapp"], [class*="open-app"],
[class*="float-bar"], [class*="floatbar"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
pointer-events: none !important;
}
/* ---------- 强制全宽,去掉左右偏移 ---------- */
#app, .web-page, .v-station-screen-content, .v-station-content,
#root, #app-root, .screen-page, .screen-page__main-content,
.screen-list, .content-wrap, vstation-screen-content {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
margin: 0 !important;
padding-left: 0 !important; padding-right: 0 !important;
height: auto !important;
overflow: visible !important;
display: block !important;
}
.screen-list {
padding: 0 !important;
background: #fff !important;
position: relative !important;
height: calc(100vh - 110px) !important;
overflow-y: auto !important; overflow-x: hidden !important;
}
.grid-waterfall.screen-list__grid {
display: block !important;
padding: 8px 12px 24px !important;
margin: 0 !important;
}
/* ---------- 顶部 #tx-header(吸顶) ---------- */
#tx-header {
position: sticky; top: 0; z-index: 20;
background: #fff;
padding: 10px 12px 0;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
text-align: left !important;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC",
"Helvetica Neue", "Microsoft YaHei", sans-serif !important;
color: #1a1a1a !important;
}
#tx-header .tx-title {
font-size: 15px; font-weight: 700; color: #1a1a1a;
margin: 0 0 8px;
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
text-align: left !important;
gap: 6px;
}
#tx-header .tx-title * { text-align: left !important; }
#tx-channels {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
gap: 8px; overflow-x: auto;
scrollbar-width: none; -webkit-overflow-scrolling: touch;
padding-bottom: 8px;
}
#tx-channels::-webkit-scrollbar { display: none; }
#tx-channels button {
flex: 0 0 auto; border: 0; background: #f0f0f3; color: #555;
padding: 7px 16px; border-radius: 16px; font-size: 14px; cursor: pointer;
white-space: nowrap; font-family: inherit !important; min-width: 0;
line-height: 1.2;
}
#tx-channels button.active { background: #e6f4ff; color: #1a8cff; font-weight: 700; }
/* ---------- 筛选标签栏(放在 header 里) ---------- */
.tx-filter-tabs {
display: flex !important;
flex-wrap: nowrap !important;
flex-direction: row !important;
gap: 8px !important;
overflow-x: auto !important;
padding: 8px 0 !important;
background: #fff !important;
align-items: center !important;
scrollbar-width: none;
border-top: 1px solid #f0f0f0;
}
.tx-filter-tabs::-webkit-scrollbar { display: none; }
.tx-tab {
flex: 0 0 auto; border: 0; background: #f0f0f3; color: #333;
padding: 6px 14px; border-radius: 16px; font-size: 13px; cursor: pointer;
white-space: nowrap; font-family: inherit !important;
line-height: 1.2;
}
.tx-tab.tx-active { background: #1a8cff; color: #fff; font-weight: 600; }
/* 隐藏原站筛选 DOM */
.filter-line { display: none !important; }
/* ---------- 居中弹窗 ---------- */
.tx-filter-modal { position: fixed; inset: 0; z-index: 99999; }
.tx-filter-mask { position: absolute; inset: 0; background: rgba(0,0,0,.45); }
.tx-filter-panel {
position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
background: #fff; border-radius: 16px; padding: 20px;
width: 320px; max-width: 90vw; max-height: 70vh; overflow-y: auto;
box-shadow: 0 8px 32px rgba(0,0,0,.2);
}
.tx-modal-title { font-size: 16px; font-weight: 700; color: #222; margin-bottom: 14px; }
.tx-opt {
display: inline-block; padding: 7px 14px; margin: 3px;
border-radius: 16px; background: #f5f5f7; border: 0; cursor: pointer;
font-size: 13px; color: #555; font-family: inherit !important;
line-height: 1.2;
}
.tx-opt.sel { background: #1a8cff; color: #fff; font-weight: 600; }
/* ---------- 卡片墙 grid ---------- */
.grid-waterfall__content, .grid-waterfall__content--web {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
gap: 10px !important;
padding: 0 !important;
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
justify-items: stretch !important;
align-items: stretch !important;
justify-content: stretch !important;
}
.screen-cover, .screen-cover--pc {
width: 100% !important; margin: 0 !important;
justify-self: stretch !important;
border-radius: 8px !important;
background: #fff !important; box-shadow: 0 1px 4px rgba(0,0,0,.08);
display: flex !important; flex-direction: column !important;
overflow: visible !important; height: auto !important;
}
.screen-cover .poster-view, .screen-cover .item-poster {
width: 100% !important; height: auto !important;
aspect-ratio: 2 / 3 !important; position: relative !important;
border-radius: 8px 8px 0 0 !important; overflow: hidden !important;
}
.screen-cover .poster-view__img {
width: 100% !important; height: 100% !important;
object-fit: cover !important; display: block !important;
}
.screen-cover .item-info { width: 100% !important; padding: 6px 8px 10px !important; overflow: visible !important; }
.screen-cover .item-title {
font-size: 13px !important; font-weight: 600 !important; color: #222 !important;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;
line-height: 1.3;
}
.screen-cover .item-secondTitle {
font-size: 11px !important; color: #999 !important; margin-top: 2px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;
line-height: 1.3;
}
.screen-cover .item-tags { display: none !important; }
/* ---------- 横版频道 ---------- */
body.tx-landscape .grid-waterfall__content,
body.tx-landscape .grid-waterfall__content--web {
grid-template-columns: repeat(2, 1fr) !important;
gap: 12px !important;
}
body.tx-landscape .screen-cover .poster-view,
body.tx-landscape .screen-cover .item-poster {
aspect-ratio: 16 / 9 !important;
}
body.tx-landscape .screen-cover .item-info { padding: 8px 10px 12px !important; height: auto !important; flex: 1; }
body.tx-landscape .screen-cover .item-title {
font-size: 14px !important; white-space: nowrap !important;
overflow: hidden !important; text-overflow: ellipsis !important;
display: block !important; line-height: 1.35;
/* 单行显示:长片名继续在右侧空白处续写,不折行 */
}
body.tx-landscape .screen-cover .item-secondTitle { margin-top: 4px; overflow: hidden !important; text-overflow: ellipsis; display: block; }
/* ---------- PC 宽屏 ---------- */
@media (min-width: 769px) {
.grid-waterfall__content, .grid-waterfall__content--web {
grid-template-columns: repeat(5, 1fr) !important;
gap: 16px !important;
}
body.tx-landscape .grid-waterfall__content,
body.tx-landscape .grid-waterfall__content--web {
grid-template-columns: repeat(4, 1fr) !important;
gap: 18px !important;
}
.grid-waterfall.screen-list__grid { padding: 16px 24px !important; }
#tx-header { padding: 12px 24px 0 !important; }
#tx-header .tx-title { font-size: 17px !important; }
#tx-channels button { padding: 8px 20px !important; font-size: 14px !important; }
.tx-filter-tabs { padding: 10px 0 !important; }
}
/* ---------- 窄屏手机 ---------- */
@media (max-width: 600px) {
#tx-header { padding: 8px 8px 0 !important; }
#tx-channels button { padding: 6px 12px; font-size: 13px; }
.grid-waterfall.screen-list__grid { padding: 6px !important; }
.grid-waterfall__content, .grid-waterfall__content--web { gap: 6px !important; }
body.tx-landscape .grid-waterfall__content,
body.tx-landscape .grid-waterfall__content--web { gap: 8px !important; }
body.tx-landscape .screen-cover .item-title { font-size: 13px !important; }
}
`;
// ---------- 注入 ----------
function txInjectViewport() {
let vp = document.querySelector('meta[name="viewport"]');
if (!vp) {
vp = document.createElement('meta');
vp.name = 'viewport';
(document.head || document.documentElement).appendChild(vp);
}
vp.content = 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no';
}
function txInjectCSS() {
if (typeof GM_addStyle === 'function') GM_addStyle(TX_CSS);
else {
const s = document.createElement('style');
s.textContent = TX_CSS;
(document.head || document.documentElement).appendChild(s);
}
}
// ---------- 顶部频道栏 ----------
function txBuildHeader() {
if (document.getElementById('tx-header')) return;
const activeId = txCurrentChannelId();
const header = document.createElement('div');
header.id = 'tx-header';
let btns = '';
TX_CHANNELS.forEach(c => {
btns += ``;
});
header.innerHTML = `
腾讯视频 · 全频道榜单
${btns}
`;
const main = document.querySelector('.screen-page__main-content') ||
document.querySelector('.screen-page') ||
document.getElementById('app-root');
if (!main) return;
main.insertBefore(header, main.firstChild);
header.querySelectorAll('#tx-channels button').forEach(btn => {
btn.addEventListener('click', () => {
const c = TX_CHANNELS.find(x => x.id === btn.dataset.id);
if (c) location.href = txChannelUrl(c);
});
});
const act = header.querySelector('#tx-channels button.active');
if (act && act.scrollIntoView) act.scrollIntoView({ inline: 'center', block: 'nearest' });
}
// ---------- 横竖版自动识别 ----------
function txDetectMode() {
const imgs = document.querySelectorAll('.poster-view__img');
let chosen = null;
imgs.forEach(im => { if (!chosen && im.complete && im.naturalWidth > 50) chosen = im; });
if (!chosen && imgs.length > 0) chosen = imgs[0];
if (!chosen) { setTimeout(txDetectMode, 500); return; }
const apply = () => {
const w = chosen.naturalWidth, h = chosen.naturalHeight;
if (w && h) {
const ratio = w / h;
if (ratio > 1.25) document.body.classList.add('tx-landscape');
else document.body.classList.remove('tx-landscape');
}
};
if (chosen.complete && chosen.naturalWidth) apply();
else chosen.addEventListener('load', apply, { once: true });
setTimeout(apply, 1000);
setTimeout(apply, 2500);
}
// ---------- 自动加载更多 ----------
function txAutoFill() {
const sl = document.querySelector('.screen-list');
if (!sl) return;
const tryScroll = () => {
if (sl.scrollHeight <= sl.clientHeight + 100) {
sl.scrollTop = sl.scrollHeight;
setTimeout(tryScroll, 1200);
}
};
setTimeout(tryScroll, 900);
}
// ---------- 兜底:干掉贴底的 fixed 浮层 ----------
function txKillBottomOverlays() {
const vh = window.innerHeight;
const vw = window.innerWidth;
document.querySelectorAll('body *').forEach(el => {
if (el.id === 'tx-header') return;
// 豁免脚本自身播放器根节点整棵子树:播放器打开后注入的底部搜索横栏(.search-container)
// 同样是 fixed+贴底+宽屏+窄高,几何上与腾讯原生下载条一致,若不豁免会被本函数误杀,
// 导致移动版从腾讯卡片唤起播放后底部搜索栏消失(红果无此函数故正常)。
if (el.closest && el.closest('#dgys2-player-root')) return;
if (el.closest && (el.closest('#tx-header') || el.closest('.tx-filter-modal'))) return;
let cs; try { cs = getComputedStyle(el); } catch (e) { return; }
if (cs.position !== 'fixed') return;
const r = el.getBoundingClientRect();
if (r.height < 20 || r.height > vh * 0.5) return;
if (r.width < vw * 0.6) return;
if (r.bottom >= vh - 8 && r.top > vh * 0.5) {
el.style.setProperty('display', 'none', 'important');
}
});
}
// ---------- 弹窗是否打开 ----------
function txIsModalOpen() {
const m = document.querySelector('.tx-filter-modal');
return !!(m && m.style.display !== 'none');
}
// ---------- 筛选:标签栏 + 居中弹窗 ----------
function txBuildFilterUI(force) {
if (!force && txIsModalOpen()) return;
const sl = document.querySelector('.screen-list');
if (!sl) return;
const lines = document.querySelectorAll('.filter-line');
if (!lines.length) return;
const prev = document.querySelector('.tx-tab.tx-active');
if (prev) txState.activeTab = prev.textContent;
document.querySelectorAll('.tx-filter-tabs, .tx-filter-modal').forEach(el => el.remove());
const tabs = document.createElement('div');
tabs.className = 'tx-filter-tabs';
const modal = document.createElement('div');
modal.className = 'tx-filter-modal';
modal.style.display = 'none';
modal.innerHTML = '';
document.body.appendChild(modal);
const panel = modal.querySelector('.tx-filter-panel');
modal.querySelector('.tx-filter-mask').addEventListener('click', () => {
modal.style.display = 'none';
});
lines.forEach(line => {
const col = line.querySelector(':scope > .filter-column');
if (!col) return;
const label = col.textContent.trim();
const opts = line.querySelectorAll(':scope > .lazy-list__content .filter');
const tab = document.createElement('button');
tab.className = 'tx-tab';
tab.textContent = label;
tab.addEventListener('click', () => {
document.querySelectorAll('.tx-tab').forEach(t => t.classList.remove('tx-active'));
tab.classList.add('tx-active');
txState.activeTab = label;
panel.innerHTML = '' + label + '
';
opts.forEach(opt => {
const b = document.createElement('button');
const isSel = opt.classList.contains('filter--selected') ||
opt.classList.contains('filter--selected--dark');
b.className = 'tx-opt' + (isSel ? ' sel' : '');
b.textContent = opt.textContent.trim();
b.addEventListener('click', () => {
const want = b.textContent.trim();
let clicked = false;
document.querySelectorAll('.filter-line').forEach(freshLine => {
const freshCol = freshLine.querySelector(':scope > .filter-column');
if (!freshCol || freshCol.textContent.trim() !== label) return;
freshLine.querySelectorAll(':scope > .lazy-list__content .filter').forEach(f => {
if (f.textContent.trim() === want) { f.click(); clicked = true; }
});
});
if (!clicked) { try { opt.click(); } catch (e) {} }
modal.style.display = 'none';
setTimeout(txBuildFilterUI, 60);
setTimeout(txBuildFilterUI, 400);
setTimeout(txBuildFilterUI, 1200);
});
panel.appendChild(b);
});
modal.style.display = 'block';
});
tabs.appendChild(tab);
});
if (txState.activeTab) {
const target = [...tabs.querySelectorAll('.tx-tab')]
.find(t => t.textContent === txState.activeTab);
if (target) target.classList.add('tx-active');
}
const header = document.getElementById('tx-header');
if (header) header.appendChild(tabs);
else sl.insertBefore(tabs, sl.firstChild);
}
// ---------- 海报点击:拦截腾讯跳转,改为弹爱看搜索弹窗(自动搜索片名,点结果弹窗播放) ----------
function txBindCardClick() {
if (window._txCardBound) return;
window._txCardBound = true;
const tryIntercept = (e) => {
// 播放器已打开时不再拦截卡片(腾讯自身根节点也叫 #app,不能用 getElementById('app') 判断)
if (document.body.classList.contains('adys-playing')) return;
const t = e.target;
if (!t || !t.closest) return;
const cover = t.closest('.screen-cover, .screen-cover--pc');
if (!cover) return;
// 片名:优先 .item-title;其次卡片内 a[title]
let title = '';
const titleEl = cover.querySelector('.item-title');
if (titleEl) title = (titleEl.textContent || '').replace(/\s+/g, ' ').trim();
if (!title) {
const a = cover.querySelector('a[title]');
if (a) title = (a.getAttribute('title') || '').trim();
}
if (!title) return; // 取不到片名就放行,避免卡死
e.preventDefault();
e.stopPropagation();
try { e.stopImmediatePropagation(); } catch (err) {}
// 海报:卡片内首张图
let poster = '';
const img = cover.querySelector('.poster-view__img, .item-poster img, img');
if (img) poster = img.src || img.getAttribute('data-src') || img.getAttribute('data-original') || '';
try { openSearchModalWithKeyword(title); }
catch (err) { try { __diag.warn('tx card search err: ' + err); } catch (_) {} }
};
// click 覆盖桌面与 React onClick;touchend 覆盖手机端提前导航
document.addEventListener('click', tryIntercept, true);
// 记录 touchstart 位置,touchend 时仅点按(位移很小)才拦截,避免列表滚动抬手误触
let txTouchX = 0, txTouchY = 0;
document.addEventListener('touchstart', (e) => {
const t = e.targetTouches && e.targetTouches[0];
if (t) { txTouchX = t.clientX; txTouchY = t.clientY; }
}, { capture: true, passive: true });
document.addEventListener('touchend', (e) => {
const t = e.changedTouches && e.changedTouches[0];
if (t) {
const dx = Math.abs(t.clientX - txTouchX), dy = Math.abs(t.clientY - txTouchY);
if (dx > 12 || dy > 12) return; // 视为滚动/滑动,不拦截
}
tryIntercept(e);
}, true);
}
// ---------- 启动 ----------
function bootTx() {
txInjectViewport();
txInjectCSS();
txBuildHeader();
txDetectMode();
txAutoFill();
txKillBottomOverlays();
txBindCardClick();
function waitAndExpand() {
const expand = document.querySelector('.filter-expand');
if (expand) {
expand.click();
setTimeout(() => txBuildFilterUI(true), 1200);
} else {
setTimeout(waitAndExpand, 300);
}
}
waitAndExpand();
setTimeout(txBuildHeader, 1500);
setTimeout(() => { txDetectMode(); txAutoFill(); txKillBottomOverlays(); }, 3000);
setTimeout(txKillBottomOverlays, 5000);
if (!window._txObserver) {
window._txObserver = observeDebounced(document.body, () => {
const main = document.querySelector('.screen-page__main-content') ||
document.querySelector('.screen-page') ||
document.getElementById('app-root');
if (main && !document.getElementById('tx-header')) txBuildHeader();
if (!txIsModalOpen() && document.querySelector('.filter-line') && !document.querySelector('.tx-filter-tabs')) {
setTimeout(() => { try { txBuildFilterUI(); } catch(e){} }, 300);
}
}, { delay: 300 });
}
}
bootTx();
}
// ==================== 9. 初始化 ====================
function init() {
// 初始化主题
initTheme();
// 数据迁移与清理
migrateLegacyData();
__diag.info('初始化完成: site=' + CURRENT_SITE + ' url=' + location.href);
// 全局浮动按钮
injectGlobalButtons();
// 腾讯片库:8 频道整合卡片墙(独立模块,内部已按 CURRENT_SITE==='tx' 守卫)
initTencentLibrary();
// 红果顶栏热榜入口
injectHongguoRankEntry();
// 红果分类/热榜自动翻页
initHongguoAutopager();
// 红果列表页海报点击拦截(详情页开关关闭时直接弹窗搜索)
initHongguoCardIntercept();
// 详情页播放按钮注入
injectPlayButton();
}
// 脚本运行于 document-start,UI 注入需等待 DOM 就绪
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();