您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
支持17个视频源的M3U8视频播放器,基于HLS.js
// ==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 * // ==/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/[email protected]/dist/hls.min.js', 'https://cdn.bootcdn.net/ajax/libs/hls.js/1.4.12/hls.min.js', 'https://unpkg.com/[email protected]/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/[email protected]/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 = ` <div class="iePlayer-panel-header"> <h3 class="iePlayer-panel-title">🎬 M3U8视频播放器 (多源版)</h3> <button class="iePlayer-close-btn">×</button> </div> <div class="iePlayer-tabs"> <button class="iePlayer-tab-btn active" data-tab="search">视频搜索</button> <button class="iePlayer-tab-btn" data-tab="direct-play">链接播放</button> </div> <div class="iePlayer-panel-body"> <div class="iePlayer-tab-content active" id="iePlayer-tab-search"> <div class="iePlayer-section"> <div class="iePlayer-source-selector"> <div class="iePlayer-source-header"> 📺 选择视频源 </div> <div class="iePlayer-source-options" id="iePlayer-source-options"></div> </div> </div> <div class="iePlayer-section"> <div class="iePlayer-search-form"> <div class="iePlayer-form-header">🔍 视频搜索</div> <input type="text" class="iePlayer-search-input" placeholder="输入视频名称搜索..."> <button class="iePlayer-search-btn">搜索视频</button> </div> </div> <div class="iePlayer-loading"> <div class="iePlayer-loading-text">🔍 搜索中... (点击取消搜索按钮可停止)</div> <div class="iePlayer-progress-container" style="display: none; margin-top: 10px;"> <div class="iePlayer-progress-bar" style="width: 100%; height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden;"> <div class="iePlayer-progress-fill" style="height: 100%; width: 0%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.3s;"></div> </div> <div class="iePlayer-progress-text" style="text-align: center; font-size: 12px; color: #666; margin-top: 5px;">0%</div> </div> </div> <div class="iePlayer-results-container"> <div class="iePlayer-results"></div> <div class="iePlayer-pagination" style="display: none;"> <button class="iePlayer-page-btn" id="iePlayer-prev-page">上一页</button> <span class="iePlayer-page-info"></span> <button class="iePlayer-page-btn" id="iePlayer-next-page">下一页</button> </div> </div> </div> <div class="iePlayer-tab-content" id="iePlayer-tab-direct-play"> <div class="iePlayer-section"> <div class="iePlayer-m3u8-form"> <div class="iePlayer-m3u8-header"> 🔗 直接播放M3U8链接 </div> <input type="text" class="iePlayer-m3u8-input" placeholder="请输入M3U8链接..."> <button class="iePlayer-m3u8-btn">播放链接</button> </div> </div> </div> </div> `; 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 = ` <label> <input type="radio" name="iePlayer-searchType" value="aggregated" ${StateModule.get('isAggregatedSearch') ? 'checked' : ''}> <span>聚合搜索</span> </label> `; sourceOptions.appendChild(aggregatedOption); // 添加单个源选项 enabledSources.forEach(([sourceKey, source]) => { const option = document.createElement('div'); option.className = 'iePlayer-source-option'; option.innerHTML = ` <label> <input type="radio" name="iePlayer-searchType" value="${sourceKey}" ${!StateModule.get('isAggregatedSearch') && StateModule.get('selectedSources').includes(sourceKey) ? 'checked' : ''}> <span>${source.name}</span> </label> `; 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 = '<div class="iePlayer-no-results">未找到相关视频</div>'; 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 += `<button class="iePlayer-play-btn" data-video-id="${video.vod_id}" data-source-key="${video.source_key}" data-source-index="${index}">${routeName}</button>`; }); } else { playSourcesHTML = '<span>暂无播放源</span>'; } } else { // 如果没有播放链接,则显示加载中 playSourcesHTML = '<span>正在加载线路...</span>'; } videoElement.innerHTML = ` <div class="iePlayer-video-header"> <div class="iePlayer-video-title">${video.vod_name}</div> ${video.source_name ? `<div class="iePlayer-source-badge">${video.source_name}</div>` : ''} </div> <div class="iePlayer-video-info"> 类型:${video.type_name || '未知'} | 年份:${video.vod_year || '未知'} | 地区:${video.vod_area || '未知'}${episodeCountInfo} </div> <div class="iePlayer-play-sources" id="play-sources-${video.vod_id}"> ${playSourcesHTML} </div> <div class="iePlayer-episode-list" id="iePlayer-episodes-${video.vod_id}"></div> `; }; // 初始渲染 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 = '<span>获取线路失败</span>'; } } catch (error) { const playSourcesDiv = videoElement.querySelector(`#play-sources-${video.vod_id}`); if(playSourcesDiv) playSourcesDiv.innerHTML = '<span>加载线路出错</span>'; } })(); } }); // 一次性添加所有元素到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 = '<div class="iePlayer-loading-episodes">加载中...</div>'; 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 = '<div class="iePlayer-no-episodes">该视频暂无播放地址</div>'; 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 = '<div class="iePlayer-no-episodes">该线路暂无可播放内容</div>'; 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 = ` <button class="iePlayer-episode-btn iePlayer-disabled" disabled> ${name} (无效链接) </button> `; } else { episodeItem.innerHTML = ` <button class="iePlayer-episode-btn" data-video-id="${videoId}" data-source-key="${sourceKey}" data-source-index="${sourceIndex}" data-episode-index="${index}" data-url="${url}" data-title="${video.vod_name} - ${name}"> ${name} </button> <button class="iePlayer-copy-btn" data-url="${url}"> 复制 </button> `; } fragment.appendChild(episodeItem); }); // 清空并一次性添加所有元素 episodeList.innerHTML = ''; episodeList.appendChild(fragment); } else { episodeList.innerHTML = '<div class="iePlayer-no-episodes">该线路暂无可播放内容</div>'; } } else { episodeList.innerHTML = '<div class="iePlayer-no-episodes">获取播放信息失败</div>'; } } catch (error) { episodeList.innerHTML = '<div class="iePlayer-no-episodes">加载失败,请稍后重试</div>'; } } } // 选集播放按钮事件 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 = ` <div class="iePlayer-player-wrapper"> <div class="iePlayer-player-header"> <div class="iePlayer-player-title">${title}</div> <button class="iePlayer-player-close">×</button> </div> <video id="iePlayer-video-${Date.now()}" controls preload="auto" style="width: 100%; height: 100%; background: #000;"> 您的浏览器不支持HTML5视频播放 </video> <div class="iePlayer-loading-player" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 16px;"> 正在加载播放器... </div> </div> `; 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(); } })();