// ==UserScript==
// @name m3u8Player
// @namespace https://github.com/lol3721987/m3u8Player
// @version 1.0.1
// @license MIT
// @description 支持17个视频源的M3U8视频播放器,基于HLS.js
// @author zjb
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-end
// @noframes
// @connect cj.lziapi.com
// @connect json.heimuer.xyz
// @connect cj.rycjapi.com
// @connect bfzyapi.com
// @connect tyyszy.com
// @connect ffzy5.tv
// @connect 360zy.com
// @connect www.iqiyizyapi.com
// @connect wolongzyw.com
// @connect jszyapi.com
// @connect dbzy.tv
// @connect mozhuazy.com
// @connect www.mdzyapi.com
// @connect api.zuidapi.com
// @connect m3u8.apiyhzy.com
// @connect api.apibdzy.com
// @connect api.wujinapi.me
// @connect ikunzyapi.com
// @connect *
// @downloadURL https://update.greasyfork.icu/scripts/544089/m3u8Player.user.js
// @updateURL https://update.greasyfork.icu/scripts/544089/m3u8Player.meta.js
// ==/UserScript==
(function() {
'use strict';
// ===== 配置模块 =====
const ConfigModule = {
// 视频源配置
API_ENDPOINT: '/api.php/provide/vod',
API_SITES_CONFIG: [
['卧龙资源', 'https://collect.wolongzy.cc', true, '/api.php/provide/vod/'],
['淘片资源', 'https://www.taopianzy.com', true, '/cjapi/mc/vod/json.html'],
['LZI资源', 'https://cj.lziapi.com', true],
['黑木耳', 'https://json.heimuer.xyz', true],
['如意资源', 'https://cj.rycjapi.com', true],
['暴风资源', 'https://bfzyapi.com', true],
['天涯资源', 'https://tyyszy.com', true],
['非凡影视', 'http://ffzy5.tv', true],
['360资源', 'https://360zy.com', true],
['iqiyi资源', 'https://www.iqiyizyapi.com', true],
['极速资源', 'https://jszyapi.com', true],
['豆瓣资源', 'https://dbzy.tv', true],
['魔爪资源', 'https://mozhuazy.com', true],
['魔都资源', 'https://www.mdzyapi.com', true],
['最大资源', 'https://api.zuidapi.com', true],
['樱花资源', 'https://m3u8.apiyhzy.com', true],
['百度云资源', 'https://api.apibdzy.com', true],
['无尽资源', 'https://api.wujinapi.me', true],
['iKun资源', 'https://ikunzyapi.com', true],
['CK资源', 'https://www.ckzy1.com', false],
['jkun资源', 'https://jkunzyapi.com', false],
['百万资源', 'https://api.bwzym3u8.com', false],
['souav资源', 'https://api.souavzy.vip', false],
['155资源', 'https://155api.com', false],
['lsb资源', 'https://apilsbzy1.com', false],
['黄色仓库', 'https://hsckzy.vip', false],
['玉兔资源', 'https://yutuzy10.com', false],
['太子资源', 'https://apidanaizi.com', false],
['黄8资源', 'https://hsckzy888.com', false],
['搜V资源', 'https://api.souavzy.vip', false],
],
get API_SITES() {
return this.API_SITES_CONFIG.reduce((acc, [name, host, enabled, customEndpoint], index) => {
const endpoint = customEndpoint || this.API_ENDPOINT;
const key = new URL(host).hostname.split('.').slice(-2, -1)[0] || `site${index}`;
acc[key] = {
api: `${host}${endpoint}`,
name,
enabled,
};
return acc;
}, {});
},
// 应用配置
CONFIG: {
PAGE_SIZE: 10,
MAX_RESULTS: 100, // 限制单次搜索返回的最大结果数
SEARCH_TIMEOUT: 8000,
STORAGE_KEYS: {
LAST_SEARCH: 'iePlayer_lastSearch',
SELECTED_SOURCES: 'iePlayer_selectedSources',
IS_AGGREGATED: 'iePlayer_isAggregated',
USER_SETTINGS: 'iePlayer_userSettings'
}
},
HLS_JS_CDNS: [
'https://cdn.jsdelivr.net/npm/hls.js@1.4.12/dist/hls.min.js',
'https://cdn.bootcdn.net/ajax/libs/hls.js/1.4.12/hls.min.js',
'https://unpkg.com/hls.js@1.4.12/dist/hls.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js',
'https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js',
'https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js',
'https://cdn.bootcdn.net/ajax/libs/hls.js/1.5.17/hls.min.js'
],
// 获取启用的视频源
getEnabledSources() {
return Object.entries(this.API_SITES).filter(([key, source]) => source.enabled);
},
// 获取视频源配置
getSource(sourceKey) {
return this.API_SITES[sourceKey] || null;
},
// 获取所有视频源
getAllSources() {
return this.API_SITES;
}
};
// ===== 存储模块 =====
const StorageModule = {
// 获取存储值
get(key, defaultValue = null) {
return GM_getValue(key, defaultValue);
},
// 设置存储值
set(key, value) {
GM_setValue(key, value);
},
// 获取最后搜索
getLastSearch() {
return this.get(ConfigModule.CONFIG.STORAGE_KEYS.LAST_SEARCH, '');
},
// 保存最后搜索
setLastSearch(keyword) {
this.set(ConfigModule.CONFIG.STORAGE_KEYS.LAST_SEARCH, keyword);
},
// 获取选中的视频源
getSelectedSources() {
return this.get(ConfigModule.CONFIG.STORAGE_KEYS.SELECTED_SOURCES, ['lziapi']);
},
// 保存选中的视频源
setSelectedSources(sources) {
this.set(ConfigModule.CONFIG.STORAGE_KEYS.SELECTED_SOURCES, sources);
},
// 获取是否聚合搜索
getIsAggregated() {
return this.get(ConfigModule.CONFIG.STORAGE_KEYS.IS_AGGREGATED, false);
},
// 保存是否聚合搜索
setIsAggregated(isAggregated) {
this.set(ConfigModule.CONFIG.STORAGE_KEYS.IS_AGGREGATED, isAggregated);
}
};
// ===== 状态管理模块 =====
const StateModule = {
// 状态数据
state: {
searchPanel: null,
currentPlayer: null,
isPlayerVisible: false,
currentPage: 1,
totalPages: 1,
currentKeyword: '',
videoPlayer: null,
selectedSources: ['lziapi'],
isAggregatedSearch: false,
isSearching: false,
searchController: null,
allSearchResults: null // 用于存储所有搜索结果
},
// 状态监听器
listeners: {},
// 获取状态
get(key) {
return this.state[key];
},
// 设置状态
set(key, value) {
const oldValue = this.state[key];
this.state[key] = value;
// 触发监听器
if (this.listeners[key]) {
this.listeners[key].forEach(callback => {
callback(value, oldValue);
});
}
},
// 批量设置状态
setState(updates) {
Object.keys(updates).forEach(key => {
this.set(key, updates[key]);
});
},
// 添加状态监听器
addListener(key, callback) {
if (!this.listeners[key]) {
this.listeners[key] = [];
}
this.listeners[key].push(callback);
},
// 移除状态监听器
removeListener(key, callback) {
if (this.listeners[key]) {
const index = this.listeners[key].indexOf(callback);
if (index > -1) {
this.listeners[key].splice(index, 1);
}
}
},
// 初始化状态
init() {
// 从存储恢复状态
this.set('isAggregatedSearch', StorageModule.getIsAggregated());
this.set('selectedSources', StorageModule.getSelectedSources());
// 初始化分页状态
this.set('currentPage', 1);
this.set('totalPages', 1);
this.set('currentKeyword', '');
}
};
// 为了兼容性,保留全局状态引用
const globalState = StateModule.state;
// ===== API模块 =====
const APIModule = {
// 单个源搜索
searchSingleSource(sourceKey, keyword, page = 1, abortController = null) {
return new Promise((resolve) => {
const source = ConfigModule.getSource(sourceKey);
if (!source) {
resolve(null);
return;
}
// 检查是否已被取消
if (abortController && abortController.signal.aborted) {
resolve(null);
return;
}
const params = new URLSearchParams({
ac: 'list',
wd: keyword,
pg: page,
limit: ConfigModule.CONFIG.PAGE_SIZE
});
const request = GM_xmlhttpRequest({
method: 'GET',
url: `${source.api}?${params}`,
timeout: ConfigModule.CONFIG.SEARCH_TIMEOUT,
onload: function(response) {
try {
// 检查是否已被取消
if (abortController && abortController.signal.aborted) {
resolve(null);
return;
}
const data = JSON.parse(response.responseText);
// 为每个结果添加源信息
if (data.list && Array.isArray(data.list)) {
data.list.forEach(item => {
item.source_name = source.name;
item.source_key = sourceKey;
});
}
resolve(data);
} catch (error) {
resolve(null);
}
},
onerror: function() {
resolve(null);
},
ontimeout: function() {
resolve(null);
}
});
// 如果有中断控制器,监听中断信号
if (abortController) {
abortController.signal.addEventListener('abort', () => {
if (request && request.abort) {
request.abort();
}
resolve(null);
});
}
});
},
// 聚合搜索(支持进度显示)
async searchAggregated(keyword, page = 1, abortController = null, progressCallback = null) {
const enabledSources = ConfigModule.getEnabledSources();
const totalSources = enabledSources.length;
let completedSources = 0;
// 更新进度的辅助函数
const updateProgress = () => {
if (progressCallback) {
const progress = Math.round((completedSources / totalSources) * 100);
progressCallback(progress, completedSources, totalSources);
}
};
// 初始化进度
updateProgress();
try {
// 使用Promise.allSettled来处理部分失败的情况
const searchPromises = enabledSources.map(async ([sourceKey]) => {
try {
const result = await this.searchSingleSource(sourceKey, keyword, page, abortController);
completedSources++;
updateProgress();
return { sourceKey, result, success: true };
} catch (error) {
completedSources++;
updateProgress();
return { sourceKey, error, success: false };
}
});
const results = await Promise.allSettled(searchPromises);
// 检查是否已被取消
if (abortController && abortController.signal.aborted) {
throw new Error('搜索已取消');
}
// 合并所有成功的结果
let allResults = [];
let totalCount = 0;
let maxPage = 0;
results.forEach(promiseResult => {
if (promiseResult.status === 'fulfilled' && promiseResult.value.success) {
const result = promiseResult.value.result;
if (result && result.list && Array.isArray(result.list)) {
// 为每个结果添加源信息
const source = ConfigModule.getSource(promiseResult.value.sourceKey);
result.list.forEach(item => {
item.source_name = source.name;
item.source_key = promiseResult.value.sourceKey;
});
allResults = allResults.concat(result.list);
// 累加总数量
if (result.total) {
totalCount += result.total;
}
// 记录最大页码
if (result.pagecount && result.pagecount > maxPage) {
maxPage = result.pagecount;
}
}
}
});
// 去重(基于视频名称和年份)
const uniqueResults = [];
const seen = new Set();
allResults.forEach(item => {
const key = `${item.vod_name}_${item.vod_year}`;
if (!seen.has(key)) {
seen.add(key);
uniqueResults.push(item);
}
});
// 按名称排序
uniqueResults.sort((a, b) => (a.vod_name || '').localeCompare(b.vod_name || ''));
return {
code: 1,
list: uniqueResults,
total: totalCount,
pagecount: maxPage
};
} catch (error) {
throw error;
}
},
// 获取视频详情
getVideoDetail(videoId, sourceKey) {
return new Promise((resolve, reject) => {
const source = ConfigModule.getSource(sourceKey);
if (!source) {
reject(new Error('无效的视频源'));
return;
}
const params = new URLSearchParams({
ac: 'detail',
ids: videoId
});
GM_xmlhttpRequest({
method: 'GET',
url: `${source.api}?${params}`,
timeout: ConfigModule.CONFIG.SEARCH_TIMEOUT,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (error) {
reject(error);
}
},
onerror: function(error) {
reject(error);
}
});
});
}
};
// ===== 工具函数模块 =====
const UtilsModule = {
// 复制到剪贴板
copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => {
this.fallbackCopyTextToClipboard(text);
});
} else {
this.fallbackCopyTextToClipboard(text);
}
},
// 兜底复制方法
fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
} catch (err) {
// 静默处理错误
}
document.body.removeChild(textArea);
},
// 防抖函数
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// 节流函数
throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
},
// 性能监控函数
perfMonitor: {
marks: new Map(),
// 标记时间点
mark(name) {
if (typeof performance !== 'undefined' && performance.mark) {
performance.mark(name);
this.marks.set(name, performance.now());
}
},
// 测量两个标记之间的时间
measure(startMark, endMark) {
if (typeof performance !== 'undefined' && performance.measure) {
try {
performance.measure(`${startMark} to ${endMark}`, startMark, endMark);
const entries = performance.getEntriesByName(`${startMark} to ${endMark}`);
if (entries.length > 0) {
return entries[0].duration;
}
} catch (e) {
// 静默处理错误
}
}
// 降级到手动计算
const start = this.marks.get(startMark);
const end = this.marks.get(endMark);
if (start !== undefined && end !== undefined) {
return end - start;
}
return null;
},
// 记录性能日志(仅在开发模式下)
log(name, duration) {
// 只在控制台中记录,不显示给用户
// 生产环境中已移除性能日志输出
}
}
};
// ===== 搜索模块 =====
const SearchModule = {
// 搜索视频
searchVideos(keyword, page = 1, abortController = null, progressCallback = null) {
if (StateModule.get('isAggregatedSearch')) {
return APIModule.searchAggregated(keyword, page, abortController, progressCallback);
} else {
const selectedSource = StateModule.get('selectedSources')[0] || 'lziapi';
return APIModule.searchSingleSource(selectedSource, keyword, page, abortController);
}
}
};
// ===== UI模块 =====
const UIModule = {
// 创建搜索面板
createSearchPanel() {
const panel = document.createElement('div');
panel.className = 'iePlayer-search-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
return panel;
},
// 初始化视频源选择器
initializeSourceSelector() {
const sourceOptions = document.getElementById('iePlayer-source-options');
if (!sourceOptions) return;
const enabledSources = ConfigModule.getEnabledSources();
// 添加聚合搜索选项
const aggregatedOption = document.createElement('div');
aggregatedOption.className = 'iePlayer-source-option';
aggregatedOption.innerHTML = `
`;
sourceOptions.appendChild(aggregatedOption);
// 添加单个源选项
enabledSources.forEach(([sourceKey, source]) => {
const option = document.createElement('div');
option.className = 'iePlayer-source-option';
option.innerHTML = `
`;
sourceOptions.appendChild(option);
});
// 添加事件监听
sourceOptions.addEventListener('change', function(e) {
const selectedValue = e.target.value;
if (selectedValue === 'aggregated') {
StateModule.set('isAggregatedSearch', true);
StateModule.set('selectedSources', []);
} else {
StateModule.set('isAggregatedSearch', false);
StateModule.set('selectedSources', [selectedValue]);
}
// 保存设置
StorageModule.setIsAggregated(StateModule.get('isAggregatedSearch'));
StorageModule.setSelectedSources(StateModule.get('selectedSources'));
});
},
// 显示/隐藏加载状态
showLoading(show) {
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
const loading = searchPanel.querySelector('.iePlayer-loading');
const searchBtn = searchPanel.querySelector('.iePlayer-search-btn');
const progressContainer = searchPanel.querySelector('.iePlayer-progress-container');
if (show) {
loading.classList.add('show');
// 只有在聚合搜索时才显示进度条
if (StateModule.get('isAggregatedSearch')) {
progressContainer.style.display = 'block';
// 重置进度
const progressFill = progressContainer.querySelector('.iePlayer-progress-fill');
const progressText = progressContainer.querySelector('.iePlayer-progress-text');
if (progressFill) progressFill.style.width = '0%';
if (progressText) progressText.textContent = '0%';
} else {
progressContainer.style.display = 'none';
}
searchBtn.disabled = false;
searchBtn.textContent = '取消搜索';
searchBtn.style.background = '#dc3545';
StateModule.set('isSearching', true);
} else {
loading.classList.remove('show');
progressContainer.style.display = 'none';
searchBtn.disabled = false;
searchBtn.textContent = '搜索视频';
searchBtn.style.background = '';
StateModule.set('isSearching', false);
}
},
// 更新聚合搜索进度
updateAggregatedSearchProgress(progress, completed, total) {
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
const progressContainer = searchPanel.querySelector('.iePlayer-progress-container');
if (!progressContainer) return;
const progressFill = progressContainer.querySelector('.iePlayer-progress-fill');
const progressText = progressContainer.querySelector('.iePlayer-progress-text');
if (progressFill) {
progressFill.style.width = `${progress}%`;
}
if (progressText) {
progressText.textContent = `${progress}% (${completed}/${total})`;
}
},
// 更新分页信息
updatePagination() {
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
const pageInfo = searchPanel.querySelector('.iePlayer-page-info');
const prevPageBtn = searchPanel.querySelector('#iePlayer-prev-page');
const nextPageBtn = searchPanel.querySelector('#iePlayer-next-page');
const currentPage = StateModule.get('currentPage');
const totalPages = StateModule.get('totalPages');
pageInfo.textContent = `第 ${currentPage}/${totalPages} 页`;
prevPageBtn.disabled = currentPage <= 1;
nextPageBtn.disabled = currentPage >= totalPages;
},
// 切换搜索面板显示
toggleSearchPanel() {
let searchPanel = StateModule.get('searchPanel');
if (!searchPanel) {
searchPanel = this.createSearchPanel();
StateModule.set('searchPanel', searchPanel);
EventModule.initSearchPanel();
}
if (searchPanel.classList.contains('show')) {
searchPanel.classList.remove('show');
} else {
searchPanel.classList.add('show');
// 恢复上次搜索
this.restoreLastSearch();
}
},
// 恢复上次搜索和设置
restoreLastSearch() {
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
// 恢复设置
StateModule.set('isAggregatedSearch', StorageModule.getIsAggregated());
StateModule.set('selectedSources', StorageModule.getSelectedSources());
// 更新界面选择状态
const radios = document.querySelectorAll('input[name="iePlayer-searchType"]');
radios.forEach(radio => {
if (StateModule.get('isAggregatedSearch') && radio.value === 'aggregated') {
radio.checked = true;
} else if (!StateModule.get('isAggregatedSearch') && StateModule.get('selectedSources').includes(radio.value)) {
radio.checked = true;
}
});
// 恢复上次搜索
const lastSearch = StorageModule.getLastSearch();
if (lastSearch) {
const searchInput = searchPanel.querySelector('.iePlayer-search-input');
searchInput.value = lastSearch;
// 不再自动触发搜索,让用户手动点击搜索按钮
}
}
};
// ===== 事件管理模块 =====
const EventModule = {
// 初始化搜索面板事件
initSearchPanel() {
const panel = StateModule.get('searchPanel');
if (!panel) return;
const closeBtn = panel.querySelector('.iePlayer-close-btn');
const searchBtn = panel.querySelector('.iePlayer-search-btn');
const searchInput = panel.querySelector('.iePlayer-search-input');
const prevPageBtn = panel.querySelector('#iePlayer-prev-page');
const nextPageBtn = panel.querySelector('#iePlayer-next-page');
const m3u8Btn = panel.querySelector('.iePlayer-m3u8-btn');
const m3u8Input = panel.querySelector('.iePlayer-m3u8-input');
const tabs = panel.querySelector('.iePlayer-tabs');
// 初始化视频源选择器
UIModule.initializeSourceSelector();
// Tab切换事件
tabs.addEventListener('click', (e) => {
if (e.target.classList.contains('iePlayer-tab-btn')) {
const tabName = e.target.dataset.tab;
// 切换按钮状态
panel.querySelectorAll('.iePlayer-tab-btn').forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
// 切换内容面板
panel.querySelectorAll('.iePlayer-tab-content').forEach(content => {
content.classList.remove('active');
});
panel.querySelector(`#iePlayer-tab-${tabName}`).classList.add('active');
}
});
// 关闭按钮事件
closeBtn.onclick = () => {
panel.classList.remove('show');
};
// 搜索按钮事件
searchBtn.onclick = this.performSearch.bind(this);
// 回车搜索
searchInput.onkeypress = (e) => {
if (e.key === 'Enter') {
this.performSearch();
}
};
// 添加输入防抖搜索
const debouncedSearch = UtilsModule.debounce(() => {
const keyword = searchInput.value.trim();
if (keyword && keyword.length >= 2) {
// 自动搜索建议可以在这里实现
}
}, 500);
searchInput.oninput = debouncedSearch;
// 分页按钮事件
prevPageBtn.onclick = () => {
const currentPage = StateModule.get('currentPage');
if (currentPage > 1) {
this.displayPageResults(currentPage - 1);
}
};
nextPageBtn.onclick = () => {
const currentPage = StateModule.get('currentPage');
const totalPages = StateModule.get('totalPages');
if (currentPage < totalPages) {
this.displayPageResults(currentPage + 1);
}
};
// M3U8链接播放事件
m3u8Btn.onclick = () => {
const url = m3u8Input.value.trim();
if (!url) {
alert('请输入M3U8链接');
return;
}
// 验证是否为有效的M3U8链接
if (!url.toLowerCase().split('?')[0].endsWith('.m3u8')) {
if (!confirm('输入的链接似乎不是M3U8链接,是否继续播放?')) {
return;
}
}
// 使用播放器模块播放
PlayerModule.openVideoPlayer(url, 'M3U8视频播放');
};
// M3U8输入框回车事件
m3u8Input.onkeypress = (e) => {
if (e.key === 'Enter') {
m3u8Btn.click();
}
};
// 拖拽功能
this.makeDraggable(panel);
},
// 执行搜索
async performSearch() {
// 性能监控开始
UtilsModule.perfMonitor.mark('search-start');
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
const searchInput = searchPanel.querySelector('.iePlayer-search-input');
const keyword = searchInput.value.trim();
// 如果正在搜索,则取消搜索
if (StateModule.get('isSearching')) {
const searchController = StateModule.get('searchController');
if (searchController) {
searchController.abort();
}
UIModule.showLoading(false);
return;
}
if (!keyword) {
alert('请输入搜索关键词');
return;
}
StateModule.set('currentKeyword', keyword);
StateModule.set('currentPage', 1); // 重置为第一页
// 保存搜索记录
StorageModule.setLastSearch(keyword);
// 创建新的中断控制器
const searchController = new AbortController();
StateModule.set('searchController', searchController);
// 显示加载状态
UIModule.showLoading(true);
// 定义进度回调函数
const progressCallback = (progress, completed, total) => {
if (StateModule.get('isAggregatedSearch')) {
UIModule.updateAggregatedSearchProgress(progress, completed, total);
}
};
try {
const results = await SearchModule.searchVideos(keyword, StateModule.get('currentPage'), searchController, progressCallback);
// 检查是否已被取消
if (searchController.signal.aborted) {
// 清理搜索控制器
StateModule.set('searchController', null);
return;
}
UIModule.showLoading(false);
this.displayResults(results);
// 清理搜索控制器
StateModule.set('searchController', null);
// 性能监控结束
UtilsModule.perfMonitor.mark('search-end');
const duration = UtilsModule.perfMonitor.measure('search-start', 'search-end');
UtilsModule.perfMonitor.log('执行搜索', duration);
} catch (error) {
UIModule.showLoading(false);
// 清理搜索控制器
StateModule.set('searchController', null);
// 性能监控结束
UtilsModule.perfMonitor.mark('search-end');
const duration = UtilsModule.perfMonitor.measure('search-start', 'search-end');
UtilsModule.perfMonitor.log('执行搜索(失败)', duration);
if (error.message === '搜索已取消') {
// 搜索被取消,不显示错误消息
return;
}
// 提供更友好的错误提示
let errorMsg = error.message;
if (errorMsg.includes('timeout')) {
errorMsg = '搜索超时,请检查网络连接后重试';
} else if (errorMsg.includes('NetworkError')) {
errorMsg = '网络连接错误,请检查网络连接后重试';
} else if (errorMsg.includes('Failed to fetch')) {
errorMsg = '请求失败,请检查网络连接后重试';
}
alert(`搜索失败: ${errorMsg}\n\n建议:\n1. 检查网络连接\n2. 刷新页面后重试\n3. 更换其他视频源`);
}
},
// 使面板可拖拽
makeDraggable(element) {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
const header = element.querySelector('.iePlayer-panel-header');
header.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === header || header.contains(e.target)) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
element.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
}
}
function dragEnd() {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
},
// 显示搜索结果
displayResults(results) {
// 性能监控开始
UtilsModule.perfMonitor.mark('displayResults-start');
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
const resultsDiv = searchPanel.querySelector('.iePlayer-results');
const pagination = searchPanel.querySelector('.iePlayer-pagination');
// 保存所有搜索结果到状态中
StateModule.set('allSearchResults', results);
StateModule.set('currentPage', 1); // 重置为第一页
if (!results || !results.list || results.list.length === 0) {
resultsDiv.innerHTML = '未找到相关视频
';
pagination.style.display = 'none';
StateModule.set('totalPages', 1);
UIModule.updatePagination();
return;
}
// 显示第一页结果
this.displayPageResults(1);
// 更新总页数
const totalPages = Math.ceil(results.list.length / ConfigModule.CONFIG.PAGE_SIZE);
StateModule.set('totalPages', totalPages);
UIModule.updatePagination();
pagination.style.display = 'flex';
// 性能监控结束
UtilsModule.perfMonitor.mark('displayResults-end');
const duration = UtilsModule.perfMonitor.measure('displayResults-start', 'displayResults-end');
UtilsModule.perfMonitor.log('显示搜索结果', duration);
},
// 显示指定页面的搜索结果
displayPageResults(page) {
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
const results = StateModule.get('allSearchResults');
if (!results || !results.list) return;
const resultsDiv = searchPanel.querySelector('.iePlayer-results');
// 使用DocumentFragment优化DOM操作
const fragment = document.createDocumentFragment();
resultsDiv.innerHTML = '';
// 获取指定页的数据进行显示
const pageSize = ConfigModule.CONFIG.PAGE_SIZE;
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const pageResults = results.list.slice(startIndex, endIndex);
pageResults.forEach(video => {
const videoElement = document.createElement('div');
videoElement.className = 'iePlayer-result-item';
fragment.appendChild(videoElement); // 先添加到fragment中,方便后续查找
const renderContent = (playUrl) => {
let playSourcesHTML = '';
let episodeCountInfo = '';
if (playUrl && playUrl.trim() !== '') {
const playSources = playUrl.split('$$$');
let episodeCount = 0;
if (playSources.length > 0) {
try {
episodeCount = playSources[0].split('#').length;
} catch (e) { /* 忽略错误 */ }
}
episodeCountInfo = ` | 集数:${episodeCount}集`;
if (playSources.length > 0) {
playSources.forEach((source, index) => {
const parts = source.split('$');
let routeName = `线路${index + 1}`;
if (parts.length > 1 && isNaN(parts[0]) && !parts[0].startsWith('http')) {
routeName = parts[0];
}
playSourcesHTML += ``;
});
} else {
playSourcesHTML = '暂无播放源';
}
} else {
// 如果没有播放链接,则显示加载中
playSourcesHTML = '正在加载线路...';
}
videoElement.innerHTML = `
类型:${video.type_name || '未知'} |
年份:${video.vod_year || '未知'} |
地区:${video.vod_area || '未知'}${episodeCountInfo}
${playSourcesHTML}
`;
};
// 初始渲染
renderContent(video.vod_play_url);
// 如果初始数据没有播放链接,则自动获取
if (!video.vod_play_url || video.vod_play_url.trim() === '') {
(async () => {
try {
const videoData = await APIModule.getVideoDetail(video.vod_id, video.source_key);
if (videoData && videoData.list && videoData.list[0]) {
// 使用获取到的新数据重新渲染
renderContent(videoData.list[0].vod_play_url);
} else {
const playSourcesDiv = videoElement.querySelector(`#play-sources-${video.vod_id}`);
if(playSourcesDiv) playSourcesDiv.innerHTML = '获取线路失败';
}
} catch (error) {
const playSourcesDiv = videoElement.querySelector(`#play-sources-${video.vod_id}`);
if(playSourcesDiv) playSourcesDiv.innerHTML = '加载线路出错';
}
})();
}
});
// 一次性添加所有元素到DOM
resultsDiv.appendChild(fragment);
// 添加事件监听
this.bindResultEvents();
// 更新分页信息
StateModule.set('currentPage', page);
UIModule.updatePagination();
},
// 绑定结果事件 - 只绑定一次,使用事件委托
bindResultEvents() {
const searchPanel = StateModule.get('searchPanel');
if (!searchPanel) return;
const resultsDiv = searchPanel.querySelector('.iePlayer-results');
// 移除之前可能存在的事件监听器
const oldHandler = resultsDiv._iePlayerHandler;
if (oldHandler) {
resultsDiv.removeEventListener('click', oldHandler);
}
// 创建新的事件处理器
const newHandler = async (e) => {
try {
// 线路按钮点击事件
if (e.target.classList.contains('iePlayer-play-btn')) {
e.preventDefault();
e.stopPropagation();
const videoId = e.target.dataset.videoId;
const sourceKey = e.target.dataset.sourceKey;
const sourceIndex = parseInt(e.target.dataset.sourceIndex);
const episodeList = document.getElementById(`iePlayer-episodes-${videoId}`);
// 切换选集列表显示
const wasHidden = !episodeList.classList.contains('show');
// 隐藏所有其他选集列表
document.querySelectorAll('.iePlayer-episode-list').forEach(el => {
el.classList.remove('show');
});
if (wasHidden) {
// 显示加载状态
episodeList.innerHTML = '加载中...
';
episodeList.classList.add('show');
try {
const videoData = await APIModule.getVideoDetail(videoId, sourceKey);
if (videoData && videoData.list && videoData.list[0]) {
const video = videoData.list[0];
if (!video.vod_play_url) {
episodeList.innerHTML = '该视频暂无播放地址
';
return;
}
// 解析播放URL - 支持多种格式
const playUrls = video.vod_play_url.split('$$$');
if (playUrls[sourceIndex]) {
const episodes = playUrls[sourceIndex].split('#').filter(ep => ep.trim());
if (episodes.length === 0) {
episodeList.innerHTML = '该线路暂无可播放内容
';
return;
}
// 使用DocumentFragment优化DOM操作
const fragment = document.createDocumentFragment();
episodes.forEach((ep, index) => {
let name, url;
// 处理多种格式的播放链接
if (ep.includes('$')) {
[name, url] = ep.split('$');
} else if (ep.includes('】')) {
// 处理 【第1集】http://example.com 格式
const match = ep.match(/【(.+?)】(.+)/);
if (match) {
name = match[1];
url = match[2];
} else {
name = `第${index + 1}集`;
url = ep;
}
} else if (ep.match(/^\d+\./)) {
// 处理 01.http://example.com 格式
const parts = ep.split('.');
if (parts.length >= 2) {
name = `第${parts[0]}集`;
url = parts.slice(1).join('.');
} else {
name = `第${index + 1}集`;
url = ep;
}
} else {
// 如果没有分隔符,整个就是URL
name = `第${index + 1}集`;
url = ep;
}
// 清理URL和名称
url = url ? url.trim() : '';
name = name ? name.trim() : `第${index + 1}集`;
const episodeItem = document.createElement('div');
episodeItem.className = 'iePlayer-episode-item';
// 验证URL有效性
if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) {
episodeItem.innerHTML = `
`;
} else {
episodeItem.innerHTML = `
`;
}
fragment.appendChild(episodeItem);
});
// 清空并一次性添加所有元素
episodeList.innerHTML = '';
episodeList.appendChild(fragment);
} else {
episodeList.innerHTML = '该线路暂无可播放内容
';
}
} else {
episodeList.innerHTML = '获取播放信息失败
';
}
} catch (error) {
episodeList.innerHTML = '加载失败,请稍后重试
';
}
}
}
// 选集播放按钮事件
if (e.target.classList.contains('iePlayer-episode-btn')) {
e.preventDefault();
e.stopPropagation();
const url = e.target.dataset.url;
const title = e.target.dataset.title;
// 验证URL
if (!url || url === 'undefined' || url === 'null') {
alert('播放链接无效,请尝试其他集数或线路');
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
alert('播放链接格式无效,请尝试其他集数或线路');
return;
}
// 高亮当前选中的集数
document.querySelectorAll('.iePlayer-episode-btn').forEach(btn => {
btn.classList.remove('active');
});
e.target.classList.add('active');
try {
// 判断URL类型并选择播放方式
// 更准确的M3U8检测
const isM3U8 = url.toLowerCase().split('?')[0].endsWith('.m3u8') ||
url.toLowerCase().includes('hls');
// 修改播放逻辑:所有M3U8链接都用内置播放器
if (isM3U8) {
PlayerModule.openVideoPlayer(url, title || '视频播放');
} else {
GM_openInTab(url, { active: true });
}
} catch (error) {
// 提供更友好的播放失败提示
let errorMsg = error.message;
if (errorMsg.includes('NetworkError') || errorMsg.includes('Failed to fetch')) {
errorMsg = '网络连接错误,无法播放视频';
} else if (errorMsg.includes('HLS')) {
errorMsg = '视频格式不支持或链接已失效';
}
alert(`播放失败: ${errorMsg}\n\n建议:\n1. 尝试其他集数或线路\n2. 检查网络连接\n3. 刷新页面后重试`);
}
}
// 复制按钮事件
if (e.target.classList.contains('iePlayer-copy-btn')) {
e.preventDefault();
e.stopPropagation();
const url = e.target.dataset.url;
UtilsModule.copyToClipboard(url);
// 显示复制成功反馈
const originalText = e.target.textContent;
e.target.textContent = '已复制';
e.target.classList.add('copied');
setTimeout(() => {
e.target.textContent = originalText;
e.target.classList.remove('copied');
}, 1000);
}
} catch (error) {
console.error('事件处理出错:', error);
// 静默处理错误,避免影响用户体验
}
};
// 保存新的事件处理器引用并绑定
resultsDiv._iePlayerHandler = newHandler;
resultsDiv.addEventListener('click', newHandler);
}
};
// ===== 播放器模块 =====
const PlayerModule = {
// 创建视频播放器
openVideoPlayer(url, title) {
// 创建播放器容器
const playerContainer = document.createElement('div');
playerContainer.className = 'iePlayer-player-container';
playerContainer.innerHTML = `
正在加载播放器...
`;
document.body.appendChild(playerContainer);
// 显示播放器
playerContainer.classList.add('show');
// 初始化容器的事件处理器存储和定时器数组
playerContainer.timers = [];
// 绑定关闭事件
const closeBtn = playerContainer.querySelector('.iePlayer-player-close');
const closeBtnHandler = () => {
this.closeVideoPlayer(playerContainer);
};
closeBtn.addEventListener('click', closeBtnHandler);
closeBtn.clickHandler = closeBtnHandler; // 保存引用便于清理
// 点击外部区域关闭
const containerClickHandler = (e) => {
if (e.target === playerContainer) {
this.closeVideoPlayer(playerContainer);
}
};
playerContainer.addEventListener('click', containerClickHandler);
playerContainer.clickHandler = containerClickHandler; // 保存引用便于清理
// 键盘事件
const keyHandler = (e) => {
if (e.key === 'Escape') {
this.closeVideoPlayer(playerContainer);
}
};
document.addEventListener('keydown', keyHandler);
playerContainer.keyHandler = keyHandler; // 保存引用便于清理
// 初始化播放器
setTimeout(() => {
this.initVideoPlayer(playerContainer, url, title, keyHandler);
}, 100);
},
// 初始化视频播放器
initVideoPlayer(container, url, title, keyHandler) {
const videoElement = container.querySelector('video');
const loadingElement = container.querySelector('.iePlayer-loading-player');
// 检查是否已经有全局HLS.js可用
if (typeof window.HlsGlobal !== 'undefined' && window.HlsGlobal.isSupported()) {
this.setupVideoPlayer(window.HlsGlobal, container, url, videoElement, loadingElement);
return;
}
// 强制清理可能存在的旧实例
if (typeof Hls !== 'undefined') {
delete window.Hls;
}
let currentCDNIndex = 0;
const tryLoadHLS = () => {
if (currentCDNIndex >= ConfigModule.HLS_JS_CDNS.length) {
if (loadingElement) {
loadingElement.textContent = '播放器库加载失败,请检查网络连接或稍后重试';
}
return;
}
const currentCDN = ConfigModule.HLS_JS_CDNS[currentCDNIndex];
const script = document.createElement('script');
script.src = currentCDN;
script.onload = () => {
// 等待一点时间确保库完全加载
setTimeout(() => {
if (typeof Hls !== 'undefined' && typeof Hls.isSupported === 'function') {
// 缓存到全局变量
window.HlsGlobal = Hls;
this.setupVideoPlayer(Hls, container, url, videoElement, loadingElement);
} else {
currentCDNIndex++;
script.remove();
tryLoadHLS();
}
}, 100);
};
script.onerror = () => {
currentCDNIndex++;
script.remove();
tryLoadHLS();
};
// 超时处理
setTimeout(() => {
if (typeof Hls === 'undefined') {
currentCDNIndex++;
script.remove();
tryLoadHLS();
}
}, 5000);
document.head.appendChild(script);
};
// 开始加载
tryLoadHLS();
},
// 设置视频播放器
setupVideoPlayer(HlsClass, container, url, videoElement, loadingElement) {
try {
// 隐藏加载提示
if (loadingElement) {
loadingElement.style.display = 'none';
}
// 检查HLS支持
if (HlsClass && HlsClass.isSupported()) {
try {
// 创建HLS实例前先清理可能存在的旧实例
if (container.hlsInstance) {
try {
container.hlsInstance.destroy();
} catch (oldHlsError) {
console.warn('清理旧HLS实例失败:', oldHlsError);
}
}
const hls = new HlsClass({
debug: false,
enableWorker: true,
lowLatencyMode: false,
backBufferLength: 90,
maxBufferLength: 30,
maxMaxBufferLength: 60,
liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 10,
// 添加CORS配置
xhrSetup: function(xhr) {
xhr.setRequestHeader('Accept', '*/*');
}
});
// 加载视频源
hls.loadSource(url);
hls.attachMedia(videoElement);
// 保存HLS实例引用,用于后续清理
container.hlsInstance = hls;
// 监听事件 - 定义可清理的事件处理器
const mediaAttachedHandler = () => {
// 媒体附加成功
};
const manifestParsedHandler = () => {
// 自动播放
videoElement.play().then(() => {
// 播放成功
}).catch(() => {
// 显示播放按钮
const playButton = document.createElement('button');
playButton.textContent = '点击播放';
playButton.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 15px 30px;
font-size: 18px;
background: rgba(0,0,0,0.8);
color: white;
border: 2px solid white;
border-radius: 8px;
cursor: pointer;
z-index: 1000;
`;
const playButtonHandler = () => {
videoElement.play();
playButton.remove();
};
playButton.addEventListener('click', playButtonHandler);
container.appendChild(playButton);
});
};
const levelLoadedHandler = () => {
// 级别加载完成
};
const fragLoadedHandler = () => {
// 片段加载完成
};
// 错误处理
const errorHandler = (event, data) => {
if (data.fatal) {
switch (data.type) {
case HlsClass.ErrorTypes.NETWORK_ERROR:
const networkRetryTimer = setTimeout(() => {
if (hls && !hls.destroyed) {
hls.startLoad();
}
}, 1000);
container.timers.push(networkRetryTimer);
break;
case HlsClass.ErrorTypes.MEDIA_ERROR:
const mediaRetryTimer = setTimeout(() => {
if (hls && !hls.destroyed) {
hls.recoverMediaError();
}
}, 1000);
container.timers.push(mediaRetryTimer);
break;
default:
if (hls && !hls.destroyed) {
hls.destroy();
}
if (loadingElement) {
loadingElement.style.display = 'block';
loadingElement.textContent = `播放失败:${data.details}\n\n可能原因:\n1. 视频源已失效\n2. 网络连接问题\n3. 视频格式不支持\n\n请尝试其他线路或稍后重试`;
}
break;
}
}
};
// 绑定事件监听器
hls.on(HlsClass.Events.MEDIA_ATTACHED, mediaAttachedHandler);
hls.on(HlsClass.Events.MANIFEST_PARSED, manifestParsedHandler);
hls.on(HlsClass.Events.LEVEL_LOADED, levelLoadedHandler);
hls.on(HlsClass.Events.FRAG_LOADED, fragLoadedHandler);
hls.on(HlsClass.Events.ERROR, errorHandler);
} catch (hlsError) {
if (loadingElement) {
loadingElement.style.display = 'block';
loadingElement.textContent = 'HLS播放器初始化失败,请尝试刷新页面';
}
}
} else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
// 原生HLS支持(Safari等)
videoElement.src = url;
// 定义可清理的事件处理器
const loadStartHandler = () => {
// 开始加载
};
const loadedMetadataHandler = () => {
videoElement.play().catch(() => {
// 播放失败,静默处理
});
};
const errorHandler = () => {
if (loadingElement) {
loadingElement.style.display = 'block';
loadingElement.textContent = `播放失败:${videoElement.error?.message || '未知错误'}\n\n请尝试其他线路`;
}
};
// 绑定事件监听器
videoElement.addEventListener('loadstart', loadStartHandler);
videoElement.addEventListener('loadedmetadata', loadedMetadataHandler);
videoElement.addEventListener('error', errorHandler);
// 保存事件处理器引用以便清理
if (!container.videoEventHandlers) {
container.videoEventHandlers = [];
}
container.videoEventHandlers.push(
{ event: 'loadstart', handler: loadStartHandler },
{ event: 'loadedmetadata', handler: loadedMetadataHandler },
{ event: 'error', handler: errorHandler }
);
} else {
if (loadingElement) {
loadingElement.style.display = 'block';
loadingElement.textContent = '当前浏览器不支持HLS播放\n\n建议使用:\n• Chrome 浏览器\n• Firefox 浏览器\n• Safari 浏览器\n\n或尝试其他线路';
}
}
// 保存播放器状态
StateModule.set('currentPlayer', container);
} catch (error) {
if (loadingElement) {
loadingElement.textContent = '播放器初始化失败: ' + error.message;
}
}
},
// 关闭视频播放器
closeVideoPlayer(container) {
try {
// 设置清理超时,防止卡死
const cleanupTimeout = setTimeout(() => {
console.warn('播放器清理超时,强制移除容器');
if (container.parentNode) {
container.remove();
}
}, 3000);
// 清理HLS.js实例
if (container.hlsInstance) {
try {
// 移除所有HLS事件监听器
container.hlsInstance.off('*');
// 停止加载并清理缓冲区
container.hlsInstance.stopLoad();
// 分离媒体元素
container.hlsInstance.detachMedia();
// 销毁实例
container.hlsInstance.destroy();
// 清除引用
container.hlsInstance = null;
} catch (hlsError) {
console.warn('HLS实例清理出错:', hlsError);
}
}
// 彻底清理视频元素
const videoElement = container.querySelector('video');
if (videoElement) {
try {
// 暂停播放
videoElement.pause();
// 清理原生HLS事件监听器
if (container.videoEventHandlers) {
container.videoEventHandlers.forEach(({ event, handler }) => {
try {
videoElement.removeEventListener(event, handler);
} catch (eventError) {
console.warn(`移除视频事件 ${event} 失败:`, eventError);
}
});
container.videoEventHandlers = [];
}
// 清空所有源
videoElement.src = '';
videoElement.srcObject = null;
// 清理缓冲区
if (videoElement.load) {
videoElement.load();
}
// 移除所有事件监听器(兜底方案)
const videoClone = videoElement.cloneNode(false);
if (videoElement.parentNode) {
videoElement.parentNode.replaceChild(videoClone, videoElement);
}
} catch (videoError) {
console.warn('视频元素清理出错:', videoError);
}
}
// 清理键盘事件监听器
if (container.keyHandler) {
try {
document.removeEventListener('keydown', container.keyHandler);
container.keyHandler = null;
} catch (keyError) {
console.warn('键盘事件清理出错:', keyError);
}
}
// 清理容器事件监听器
if (container.clickHandler) {
try {
container.removeEventListener('click', container.clickHandler);
container.clickHandler = null;
} catch (clickError) {
console.warn('容器点击事件清理出错:', clickError);
}
}
// 清理关闭按钮事件
const closeBtn = container.querySelector('.iePlayer-player-close');
if (closeBtn && closeBtn.clickHandler) {
try {
closeBtn.removeEventListener('click', closeBtn.clickHandler);
closeBtn.clickHandler = null;
} catch (closeBtnError) {
console.warn('关闭按钮事件清理出错:', closeBtnError);
}
}
// 清理所有定时器
if (container.timers) {
container.timers.forEach(timer => {
try {
clearTimeout(timer);
} catch (timerError) {
console.warn('定时器清理出错:', timerError);
}
});
container.timers = [];
}
// 强制垃圾回收(如果可用)
if (window.gc && typeof window.gc === 'function') {
setTimeout(() => {
try {
window.gc();
} catch (gcError) {
// 静默处理GC错误
}
}, 100);
}
// 移除容器
if (container.parentNode) {
container.remove();
}
// 清理全局状态
if (StateModule.get('currentPlayer') === container) {
StateModule.set('currentPlayer', null);
}
// 清除清理超时
clearTimeout(cleanupTimeout);
} catch (error) {
console.error('播放器关闭时发生错误:', error);
// 强制清理,即使有错误
try {
if (container.parentNode) {
container.remove();
}
if (StateModule.get('currentPlayer') === container) {
StateModule.set('currentPlayer', null);
}
} catch (forceCleanError) {
console.error('强制清理失败:', forceCleanError);
}
}
}
};;
// 初始化样式
function initStyles() {
GM_addStyle(`
/* 主面板样式 */
.iePlayer-search-panel {
position: fixed; top: 20px; right: 20px; width: 400px; max-height: 85vh;
background: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 999999; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;
display: none; overflow: hidden;
}
.iePlayer-search-panel.show { display: block; }
.iePlayer-panel-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; padding: 15px; display: flex; justify-content: space-between;
align-items: center; cursor: move;
}
.iePlayer-panel-title { font-size: 16px; font-weight: 600; margin: 0; }
.iePlayer-close-btn {
background: none; border: none; color: white; font-size: 20px; cursor: pointer;
padding: 0; width: 24px; height: 24px; display: flex; align-items: center;
justify-content: center; border-radius: 50%; transition: background-color 0.2s;
}
.iePlayer-close-btn:hover { background-color: rgba(255,255,255,0.2); }
.iePlayer-panel-body { padding: 0 15px 15px; max-height: 70vh; overflow-y: auto; }
.iePlayer-tabs { display: flex; background-color: #f1f3f5; padding: 3px 15px 0; }
.iePlayer-tab-btn {
padding: 10px 15px; cursor: pointer; border: none; background-color: transparent;
font-size: 14px; font-weight: 500; color: #868e96; position: relative;
transition: color 0.3s;
}
.iePlayer-tab-btn:hover { color: #495057; }
.iePlayer-tab-btn.active { color: #667eea; }
.iePlayer-tab-btn.active::after {
content: ''; position: absolute; bottom: -1px; left: 0; right: 0;
height: 3px; background-color: #667eea; border-radius: 3px 3px 0 0;
}
.iePlayer-tab-content { display: none; padding-top: 15px; }
.iePlayer-tab-content.active { display: block; }
.iePlayer-section {
margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee;
}
.iePlayer-section:last-child {
margin-bottom: 0; padding-bottom: 0; border-bottom: none;
}
.iePlayer-form-header {
font-weight: 600; color: #495057; font-size: 14px; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px;
}
/* 视频源选择器样式 */
.iePlayer-source-selector, .iePlayer-search-form, .iePlayer-m3u8-form {
padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;
}
.iePlayer-source-header, .iePlayer-m3u8-header {
display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
font-weight: 600; color: #495057; font-size: 14px;
}
.iePlayer-source-options {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
max-height: 150px; overflow-y: auto;
}
.iePlayer-source-option { display: flex; align-items: center; }
.iePlayer-source-option label {
display: flex; align-items: center; gap: 6px; padding: 6px 10px;
border-radius: 6px; cursor: pointer; font-size: 12px; color: #495057;
transition: all 0.2s ease; background: white; border: 1px solid #dee2e6;
width: 100%;
}
.iePlayer-source-option label:hover,
.iePlayer-source-option label:has(input[type="radio"]:checked) {
background: #667eea; color: white; border-color: #667eea;
}
.iePlayer-source-option input[type="radio"] {
margin: 0; accent-color: #667eea;
}
.iePlayer-source-option input[type="radio"]:checked + span {
font-weight: 500;
}
.iePlayer-search-input, .iePlayer-m3u8-input {
width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px;
font-size: 14px; transition: border-color 0.2s; box-sizing: border-box;
margin-bottom: 12px;
}
.iePlayer-search-input:focus, .iePlayer-m3u8-input:focus {
outline: none; border-color: #667eea;
}
.iePlayer-search-btn, .iePlayer-m3u8-btn {
width: 100%; padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 6px; font-size: 14px;
font-weight: 500; cursor: pointer; transition: all 0.3s;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.iePlayer-m3u8-btn {
background: linear-gradient(135deg, #28a745 0%, #23843a 100%);
}
.iePlayer-search-btn:hover, .iePlayer-m3u8-btn:hover {
transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.iePlayer-search-btn:disabled {
opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none;
}
.iePlayer-loading {
text-align: center; padding: 25px; color: #666; display: none;
background: #f8f9fa; border-radius: 8px; margin: 15px 0;
}
.iePlayer-loading.show { display: block; }
.iePlayer-results-container { margin-top: 15px; }
.iePlayer-results {
max-height: 400px; overflow-y: auto; padding: 5px;
}
.iePlayer-result-item {
border: 1px solid #e1e5e9; border-radius: 6px; padding: 15px;
margin-bottom: 10px; background: #f8f9fa; transition: box-shadow 0.2s;
}
.iePlayer-result-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.iePlayer-video-header {
display: flex; justify-content: space-between; align-items: flex-start;
margin-bottom: 8px;
}
.iePlayer-video-title {
font-size: 16px; font-weight: 600; color: #333; flex: 1; line-height: 1.3;
}
.iePlayer-source-badge {
background: #667eea; color: white; padding: 3px 8px; border-radius: 12px;
font-size: 11px; font-weight: 500; white-space: nowrap; margin-left: 10px;
}
.iePlayer-video-info {
font-size: 12px; color: #666; margin-bottom: 12px; line-height: 1.4;
}
.iePlayer-play-sources { display: flex; gap: 8px; margin-bottom: 10px; }
.iePlayer-play-btn, .iePlayer-copy-btn {
padding: 6px 12px; border: 1px solid #667eea; background: white;
color: #667eea; border-radius: 4px; cursor: pointer;
font-size: 12px; transition: all 0.2s;
}
.iePlayer-copy-btn {
padding: 4px 8px; border: 1px solid #28a745; color: #28a745;
}
.iePlayer-play-btn:hover, .iePlayer-copy-btn:hover {
background: #667eea; color: white;
}
.iePlayer-copy-btn:hover { background: #28a745; }
.iePlayer-copy-btn.copied { background: #28a745; color: white; }
.iePlayer-episode-list {
display: none !important; margin-top: 10px; padding-top: 10px;
border-top: 1px solid #e1e5e9;
}
.iePlayer-episode-list.show { display: block !important; }
.iePlayer-episode-item { display: inline-flex; margin: 3px; gap: 3px; }
.iePlayer-episode-btn {
padding: 4px 8px; border: 1px solid #ddd; border-radius: 3px;
background: #f8f9fa; cursor: pointer; font-size: 11px;
transition: all 0.2s;
}
.iePlayer-episode-btn.iePlayer-disabled {
background: #e9ecef; color: #6c757d; cursor: not-allowed; opacity: 0.6;
}
.iePlayer-episode-btn:hover { background: #e9ecef; }
.iePlayer-episode-btn.active {
background: #667eea; color: white; border-color: #667eea;
}
.iePlayer-pagination {
display: flex; justify-content: space-between; align-items: center;
margin-top: 20px; padding: 15px; background: #f8f9fa;
border-radius: 8px; border: 1px solid #e9ecef;
}
.iePlayer-page-btn {
padding: 8px 16px; border: 1px solid #667eea; background: white;
color: #667eea; border-radius: 4px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.iePlayer-page-btn:hover:not(:disabled) {
background: #667eea; color: white; transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.iePlayer-page-btn:disabled {
opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none;
}
.iePlayer-page-info {
font-size: 13px; color: #666; font-weight: 500;
}
.iePlayer-no-results, .iePlayer-loading-episodes {
text-align: center; padding: 20px; color: #666;
}
.iePlayer-no-results { font-size: 14px; }
.iePlayer-loading-episodes { font-size: 13px; }
.iePlayer-no-episodes {
text-align: center; padding: 20px; color: #666; font-size: 13px;
background: #f8f9fa; border-radius: 6px; border: 1px dashed #dee2e6;
}
/* 浮动按钮样式 */
.iePlayer-float-btn {
position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%; border: none; color: white; font-size: 24px;
cursor: pointer; box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 999998; transition: all 0.3s;
display: flex; align-items: center; justify-content: center;
}
.iePlayer-float-btn:hover {
transform: scale(1.1); box-shadow: 0 6px 25px rgba(0,0,0,0.3);
}
/* M3U8 链接播放按钮样式 */
.iePlayer-m3u8-play-btn {
display: inline-block; margin-left: 10px; padding: 4px 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 4px; cursor: pointer;
font-size: 12px; text-decoration: none; transition: opacity 0.2s;
}
.iePlayer-m3u8-play-btn:hover { opacity: 0.9; }
/* 播放器容器样式 */
.iePlayer-player-container {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.9); z-index: 1000000; display: none;
align-items: center; justify-content: center;
}
.iePlayer-player-container.show { display: flex; }
.iePlayer-player-wrapper {
width: 90%; max-width: 1200px; height: 70%; position: relative;
background: #000; border-radius: 8px; overflow: hidden;
}
.iePlayer-player-header {
position: absolute; top: 0; left: 0; right: 0; height: 50px;
background: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent);
display: flex; align-items: center; justify-content: space-between;
padding: 0 20px; z-index: 1000001; color: white;
opacity: 0; transition: opacity 0.3s;
}
.iePlayer-player-wrapper:hover .iePlayer-player-header { opacity: 1; }
.iePlayer-player-title { font-size: 16px; font-weight: 500; }
.iePlayer-player-close {
background: none; border: none; color: white; font-size: 24px;
cursor: pointer; padding: 0; width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%; transition: background-color 0.2s;
}
.iePlayer-player-close:hover { background-color: rgba(255,255,255,0.2); }
`);
}
function toggleSearchPanel() {
UIModule.toggleSearchPanel();
}
// 检测和注入M3U8播放按钮
function injectM3U8PlayButtons() {
const m3u8Regex = /(https?:\/\/[^\s"'`<>?#]*\.m3u8(?:\?[^\s"'`<>#]*)?(?:#[^\s"'`<>?]*)?)/gi;
const createPlayButton = (url) => {
const playButton = document.createElement('button');
playButton.className = 'iePlayer-m3u8-play-btn';
playButton.textContent = '播放';
playButton.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
PlayerModule.openVideoPlayer(url, '视频播放');
};
return playButton;
};
// 处理链接
document.querySelectorAll('a[href]:not([data-ieplayer-injected])').forEach(link => {
m3u8Regex.lastIndex = 0; // Reset regex state
const match = m3u8Regex.exec(link.href);
if (match) {
const m3u8Url = match[0]; // The first full match
if (!link.nextElementSibling || !link.nextElementSibling.classList.contains('iePlayer-m3u8-play-btn')) {
link.dataset.iePlayerInjected = 'true';
link.parentNode.insertBefore(createPlayButton(m3u8Url), link.nextSibling);
}
}
});
// 处理文本节点
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
const parent = node.parentElement;
if (!parent || parent.isContentEditable || parent.closest('SCRIPT, STYLE, A, .iePlayer-m3u8-play-btn') || parent.dataset.ieplayerTextInjected) {
return NodeFilter.FILTER_REJECT;
}
return m3u8Regex.test(node.textContent) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
while (walker.nextNode()) {
const node = walker.currentNode;
const parent = node.parentElement;
parent.dataset.ieplayerTextInjected = 'true';
const fragment = document.createDocumentFragment();
let lastIndex = 0;
node.textContent.replace(m3u8Regex, (match, url, offset) => {
fragment.appendChild(document.createTextNode(node.textContent.substring(lastIndex, offset)));
fragment.appendChild(document.createTextNode(url));
fragment.appendChild(createPlayButton(url));
lastIndex = offset + match.length;
});
fragment.appendChild(document.createTextNode(node.textContent.substring(lastIndex)));
parent.replaceChild(fragment, node);
}
}
// 监听DOM变化
function observeDOM() {
let scheduled = false;
const observer = new MutationObserver(() => {
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
injectM3U8PlayButtons();
scheduled = false;
});
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 添加菜单命令
function addMenuCommands() {
GM_registerMenuCommand('🎬 打开视频搜索', toggleSearchPanel);
}
// 主初始化函数
function init() {
// 确保只在顶层窗口运行,避免在iframe中重复执行
if (window.self !== window.top) {
return;
}
// 检查是否为播放器页面
if (window.location.href.includes('player.html')) {
return;
}
// 初始化状态模块
StateModule.init();
// 初始化样式
initStyles();
// 注入M3U8播放按钮
injectM3U8PlayButtons();
// 监听DOM变化
observeDOM();
// 添加菜单命令
addMenuCommands();
}
// 等待页面加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();