您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持多源浏览、分类筛选和搜索的M3U8视频播放器,基于HLS.js
// ==UserScript== // @name m3u8Player // @namespace https://github.com/lol3721987/m3u8Player // @version 1.2.5 // @license MIT // @description 支持多源浏览、分类筛选和搜索的M3U8视频播放器,基于HLS.js // @author zjb & Gemini // @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 dadiapi.com // @connect slapibf.com // @connect aosikazy.com // @connect apiyutu.com // @connect thzy1.me // @connect apilsbzy1.com // @connect cj.yayazy.net // @connect hhzyapi.com // @connect www.jkunzyapi.com // @connect naixxzy.com // @connect 155api.com // @connect apilj.com // @connect caiji.semaozy.net // @connect siwazyw.tv // @connect api.bwzyz.com // @connect api.souavzy.vip // @connect www.xxibaozyw.com // @connect hsckzy.vip // @connect xingba111.com // @connect iqiyizyapi.com // @connect apidanaizi.com // @connect xzybb1.com // @connect api.ddapi.cc // @connect api.sexnguon.com // @connect www.jingpinx.com // @connect shayuapi.com // @connect www.hongniuzy2.com // @connect www.huyaapi.com // @connect caiji.maotaizy.cc // @connect api.zuiseapi.com // @connect wukongzyz.com // @connect jyzyapi.com // @connect api.xiaojizy.live // @connect api.ukuapi.com // @connect suoniapi.com // @connect 91md.me // @connect collect.wolongzyw.com // @connect sdzyapi.com // @connect www.bt4.cc // @connect api.guangsuapi.com // @connect www.hongniuzy2.com // @connect www.36717.info // @connect api.ffzyapi.com // @connect www.caoliuzyw.com // @connect cj.maczy.me // @connect 360zyzz.com // @connect www.sufeizy.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], ['大地', 'https://dadiapi.com', true, '/api.php'], ['森林', 'https://slapibf.com', true, '/api.php/provide/vod/'], ['奥斯卡', 'https://aosikazy.com', true, '/api.php/provide/vod/'], ['爱困', 'https://ikunzyapi.com', true, '/api.php/provide/vod'], ['玉兔', 'https://apiyutu.com', true, '/api.php/provide/vod/at/xml'], ['桃花', 'https://thzy1.me', true, '/api.php/provide/vod/'], ['lsp', 'https://apilsbzy1.com', true, '/api.php/provide/vod/at/xml'], ['丫丫', 'https://cj.yayazy.net', true, '/api.php/provide/vod/from/yym3u8/at/xml'], ['豪华', 'https://hhzyapi.com', true, '/api.php/provide/vod'], ['jkun', 'https://www.jkunzyapi.com', true, '/api.php/provide/vod/'], ['奶香香', 'https://naixxzy.com', true, '/api.php/provide/vod/at/xml/'], ['155', 'https://155api.com', true, '/api.php/provide/vod/at/xml/'], ['辣椒', 'https://apilj.com', true, '/api.php/provide/vod/at/xml/'], ['色猫', 'https://caiji.semaozy.net', true, '/inc/api.php'], ['丝袜', 'https://siwazyw.tv', true, '/api.php/provide/vod/at/xml/'], ['百万', 'https://api.bwzyz.com', true, '/api.php/provide/vod/at/json/'], ['搜av', 'https://api.souavzy.vip', true, '/api.php/provide/vod/'], ['x细胞', 'https://www.xxibaozyw.com', true, '/api.php/provide/vod/'], ['黄色仓库', 'https://hsckzy.vip', true, '/api.php/provide/vod/at/json/'], ['性吧', 'https://xingba111.com', true, '/api.php/provide/vod/at/xml'], ['大奶子', 'https://apidanaizi.com', true, '/api.php/provide/vod/at/xml/'], ['杏资源', 'https://xzybb1.com', true, '/api.php/provide/vod/at/xml'], ['滴滴', 'https://api.ddapi.cc', true, '/api.php/provide/vod/'], ['色南国', 'https://api.sexnguon.com', true, '/api.php/provide/vod/'], ['精品', 'http://www.jingpinx.com', true, '/api.php/provide/vod/'], ['鲨鱼', 'https://shayuapi.com', true, '/api.php/provide/vod/'], ['红牛', 'https://www.hongniuzy2.com', true, '/api.php/provide/vod/'], ['虎牙', 'https://www.huyaapi.com', true, '/api.php/provide/vod/at/json/'], ['茅台', 'https://caiji.maotaizy.cc', true, '/api.php/provide/vod/'], ['醉色', 'https://api.zuiseapi.com', true, '/api.php/provide/vod/'], ['悟空', 'https://wukongzyz.com', true, '/api.php/provide/vod/'], ['金鹰', 'https://jyzyapi.com', true, '/provide/vod/'], ['小鸡', 'https://api.xiaojizy.live', true, '/provide/vod'], ['uku', 'https://api.ukuapi.com', true, '/api.php/provide/vod/'], ['索尼', 'https://suoniapi.com', true, '/api.php/provide/vod/'], ['91制片厂', 'https://91md.me', true, '/api.php/provide/vod/'], ['卧龙', 'https://collect.wolongzyw.com', true, '/api.php/provide/vod/'], ['闪电', 'https://sdzyapi.com', true, '/api.php/provide/vod/'], ['bt4', 'http://www.bt4.cc', true, '/api.php/provide/vod/'], ['光速', 'https://api.guangsuapi.com', true, '/api.php/provide/vod/'], ['36717', 'http://www.36717.info', true, '/api.php/provide/vod/'], ['飞飞', 'http://api.ffzyapi.com', true, '/api.php/provide/vod/'], ['草榴', 'https://www.caoliuzyw.com', true, '/api.php/provide/vod/'], ['mac', 'https://cj.maczy.me', true, '/api.php/provide/vod/'], ['360', 'https://360zyzz.com', true, '/api.php/provide/vod/'], ['苏菲', 'https://www.sufeizy.com', true, '/api.php/provide/vod/'], ], get API_SITES() { return this.API_SITES_CONFIG.reduce((acc, [name, host, enabled, customEndpoint]) => { const endpoint = customEndpoint || this.API_ENDPOINT; let key; try { const urlObj = new URL(host); const hostnameParts = urlObj.hostname.split('.'); key = hostnameParts.length >= 2 ? hostnameParts[hostnameParts.length - 2] : hostnameParts[0]; } catch (e) { key = `site${Object.keys(acc).length}`; } let uniqueKey = key; let counter = 0; while (acc[uniqueKey]) { uniqueKey = `${key}${counter++}`; } acc[uniqueKey] = { api: `${host}${endpoint}`, name, enabled }; return acc; }, {}); }, CONFIG: { PAGE_SIZE: 12, SEARCH_TIMEOUT: 8000, MAX_HISTORY_ITEMS: 50, STORAGE_KEYS: { LAST_SEARCH: 'iePlayer_lastSearch', SELECTED_SOURCES: 'iePlayer_selectedSources', IS_AGGREGATED: 'iePlayer_isAggregated', PLAY_HISTORY: 'iePlayer_playHistory' } }, HLS_JS_CDNS: [ 'https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js', 'https://cdn.bootcdn.net/ajax/libs/hls.js/1.4.12/hls.min.js' ], getEnabledSources() { return Object.entries(this.API_SITES).filter(([, source]) => source.enabled); }, getSource(sourceKey) { return this.API_SITES[sourceKey] || null; }, }; // ===== 存储模块 ===== const StorageModule = { get(key, def) { return GM_getValue(key, def); }, set(key, val) { GM_setValue(key, val); }, 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); }, getPlayHistory() { return this.get(ConfigModule.CONFIG.STORAGE_KEYS.PLAY_HISTORY, []); }, setPlayHistory(history) { this.set(ConfigModule.CONFIG.STORAGE_KEYS.PLAY_HISTORY, history); }, addPlayHistory(videoData) { let history = this.getPlayHistory(); const { url, title, source, episode } = videoData; const existingIndex = history.findIndex(item => item.url === url); if (existingIndex > -1) { history.splice(existingIndex, 1); } history.unshift({ id: `hist_${Date.now()}`, url, title, source, episode, time: Date.now() }); if (history.length > ConfigModule.CONFIG.MAX_HISTORY_ITEMS) { history.pop(); } this.setPlayHistory(history); }, removePlayHistory(id) { let history = this.getPlayHistory(); this.setPlayHistory(history.filter(item => item.id !== id)); }, clearPlayHistory() { this.setPlayHistory([]); }, }; // ===== 状态管理模块 ===== const StateModule = { state: { searchPanel: null, currentPlayer: null, currentPage: 1, totalPages: 1, homepageCurrentPage: 1, homepageTotalPages: 1, homepageCategories: {}, homepageSelectedCategory: null, selectedSources: ['lziapi'], isAggregatedSearch: false, isSearching: false, searchController: null, playHistory: [] }, get(key) { return this.state[key]; }, set(key, val) { this.state[key] = val; }, init() { this.set('isAggregatedSearch', StorageModule.getIsAggregated()); this.set('selectedSources', StorageModule.getSelectedSources()); this.set('playHistory', StorageModule.getPlayHistory()); } }; // ===== API模块 ===== const APIModule = { _request(url, params) { return new Promise((resolve, reject) => { const fullUrl = `${url}?${params.toString()}`; GM_xmlhttpRequest({ method: 'GET', url: fullUrl, timeout: ConfigModule.CONFIG.SEARCH_TIMEOUT, onload: (res) => { try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(new Error('JSON parsing failed')); } }, onerror: () => reject(new Error('Request failed')), ontimeout: () => reject(new Error('Request timed out')), }); }); }, async fetchCategories(sourceKey) { const source = ConfigModule.getSource(sourceKey); if (!source) return []; try { const data = await this._request(source.api, new URLSearchParams()); return data.class || []; } catch (error) { console.error(`Failed to fetch categories from ${source.name}:`, error); return []; } }, async fetchSourceHomepage(sourceKey, page, categoryId) { const source = ConfigModule.getSource(sourceKey); if (!source) return null; const params = new URLSearchParams({ pg: page, limit: ConfigModule.CONFIG.PAGE_SIZE }); if (categoryId) { params.set('t', categoryId); } try { const data = await this._request(source.api, params); if (data.list) { data.list.forEach(item => { item.source_name = source.name; item.source_key = sourceKey; }); } return data; } catch (error) { return null; } }, search(sourceKey, keyword, page, abortSignal) { return new Promise((resolve, reject) => { const source = ConfigModule.getSource(sourceKey); if (!source) return reject(new Error('Invalid source')); const params = new URLSearchParams({ ac: 'list', wd: keyword, pg: page }); const req = GM_xmlhttpRequest({ method: 'GET', url: `${source.api}?${params.toString()}`, onload: (res) => { try { const data = JSON.parse(res.responseText); if (data.list) data.list.forEach(item => { item.source_name = source.name; item.source_key = sourceKey; }); resolve(data); } catch (e) { reject(e); } }, onerror: reject, }); if (abortSignal) abortSignal.onabort = () => req.abort(); }); }, getVideoDetail(videoId, sourceKey) { return this._request(ConfigModule.getSource(sourceKey).api, new URLSearchParams({ ac: 'detail', ids: videoId })); }, }; // ===== 工具函数模块 ===== const UtilsModule = { copyToClipboard(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text); } else { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); } } }; // ===== UI模块 ===== const UIModule = { _extractAndFormatImageUrl(picString, sourceKey) { if (!picString) return ''; const firstPart = picString.split('$$$')[0]; const urlMatch = firstPart.match(/https?:\/\/[^\s"]+\.(?:jpg|jpeg|png|gif|webp)|^\/[^\s"]+\.(?:jpg|jpeg|png|gif|webp)/i); let picUrl = urlMatch ? urlMatch[0] : ''; if (!picUrl) return ''; if (picUrl.startsWith('http')) return picUrl; try { const source = ConfigModule.getSource(sourceKey); const origin = new URL(source.api).origin; return `${origin}${picUrl.startsWith('/') ? picUrl : '/' + picUrl}`; } catch (e) { return ''; } }, 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="homepage">资源首页</button> <button class="iePlayer-tab-btn" data-tab="direct-play">链接播放</button> <button class="iePlayer-tab-btn" data-tab="history">播放历史</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-header">📺 选择视频源</div><div class="iePlayer-source-options" id="iePlayer-source-options"></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" style="display:none;">搜索中...</div> <div class="iePlayer-results"></div> <div class="iePlayer-pagination" style="display:none;"><button id="iePlayer-prev-page" class="iePlayer-page-btn">上一页</button><span class="iePlayer-page-info"></span><button id="iePlayer-next-page" class="iePlayer-page-btn">下一页</button></div> </div> <div class="iePlayer-tab-content" id="iePlayer-tab-homepage"> <div class="iePlayer-homepage-categories"></div> <div class="iePlayer-homepage-loading" style="display:none;">加载中...</div> <div class="iePlayer-homepage-tip"></div> <div class="iePlayer-homepage-results"></div> <div class="iePlayer-homepage-pagination" style="display:none;"><button id="iePlayer-prev-homepage" class="iePlayer-page-btn">上一页</button><span class="iePlayer-homepage-page-info"></span><button id="iePlayer-next-homepage" class="iePlayer-page-btn">下一页</button></div> </div> <div class="iePlayer-tab-content" id="iePlayer-tab-direct-play"><div class="iePlayer-m3u8-form"><div class="iePlayer-m3u8-header">🔗 直接播放</div><input type="text" class="iePlayer-m3u8-input" placeholder="输入M3U8链接..."><button class="iePlayer-m3u8-btn">播放</button></div></div> <div class="iePlayer-tab-content" id="iePlayer-tab-history"><div class="iePlayer-history-header"><div class="iePlayer-history-title">📚 播放历史</div><button class="iePlayer-clear-history-btn">清空</button></div><div class="iePlayer-history-list"></div><div class="iePlayer-history-empty" style="display:none;">暂无历史</div></div> </div>`; document.body.appendChild(panel); return panel; }, initializeSourceSelector() { const container = document.getElementById('iePlayer-source-options'); if (!container) return; container.innerHTML = ''; [{key: 'aggregated', name: '聚合搜索'}, ...ConfigModule.getEnabledSources().map(([key, val])=>({key, ...val}))].forEach(source => { const isAgg = source.key === 'aggregated'; const isChecked = isAgg ? StateModule.get('isAggregatedSearch') : StateModule.get('selectedSources').includes(source.key); const opt = document.createElement('div'); opt.className = 'iePlayer-source-option'; opt.innerHTML = `<label><input type="radio" name="iePlayer-searchType" value="${source.key}" ${isChecked ? 'checked' : ''}><span>${source.name}</span></label>`; container.appendChild(opt); }); }, displayCategories(categories) { const container = document.querySelector('.iePlayer-homepage-categories'); if (!container) return; container.innerHTML = ''; [{type_id: null, type_name: '最新'}, ...categories].forEach(cat => { const btn = document.createElement('button'); btn.className = 'iePlayer-category-btn'; btn.textContent = cat.type_name; btn.dataset.id = cat.type_id; if (String(StateModule.get('homepageSelectedCategory')) === String(cat.type_id)) btn.classList.add('active'); container.appendChild(btn); }); }, renderVideoList(container, videoList) { container.innerHTML = ''; const fragment = document.createDocumentFragment(); videoList.forEach(video => { const item = document.createElement('div'); item.className = 'iePlayer-result-item'; const imageUrl = this._extractAndFormatImageUrl(video.vod_pic, video.source_key); item.innerHTML = ` <div class="iePlayer-video-thumb"> <img src="${imageUrl}" alt="" loading="lazy" onerror="this.style.display='none'; this.parentElement.classList.add('no-image');"> </div> <div class="iePlayer-video-details"> <div class="iePlayer-video-header"><div class="iePlayer-video-title">${video.vod_name}</div><div class="iePlayer-source-badge">${video.source_name}</div></div> <div class="iePlayer-video-info">${video.type_name || '未知'} | ${video.vod_year || '未知'} | ${video.vod_remarks || '无'}</div> <div class="iePlayer-play-sources" data-video-id="${video.vod_id}" data-source-key="${video.source_key}"> <button class="iePlayer-load-sources-btn">加载播放线路</button> </div> <div class="iePlayer-episode-list"></div> </div>`; fragment.appendChild(item); }); container.appendChild(fragment); }, updatePagination(type) { const isHomepage = type === 'homepage'; const pageInfo = document.querySelector(isHomepage ? '.iePlayer-homepage-page-info' : '.iePlayer-page-info'); const prevBtn = document.getElementById(isHomepage ? 'iePlayer-prev-homepage' : 'iePlayer-prev-page'); const nextBtn = document.getElementById(isHomepage ? 'iePlayer-next-homepage' : 'iePlayer-next-page'); const currentPage = StateModule.get(isHomepage ? 'homepageCurrentPage' : 'currentPage'); const totalPages = StateModule.get(isHomepage ? 'homepageTotalPages' : 'totalPages'); if (pageInfo) pageInfo.textContent = `第 ${currentPage} / ${totalPages} 页`; if (prevBtn) prevBtn.disabled = currentPage <= 1; if (nextBtn) nextBtn.disabled = currentPage >= totalPages; }, toggleSearchPanel() { let searchPanel = StateModule.get('searchPanel'); if (!searchPanel) { searchPanel = this.createSearchPanel(); StateModule.set('searchPanel', searchPanel); EventModule.initSearchPanel(); } const isVisible = searchPanel.classList.toggle('show'); if (isVisible) { searchPanel.querySelector('.iePlayer-search-input').value = StorageModule.getLastSearch(); } }, displayPlayHistory() { const list = document.querySelector('.iePlayer-history-list'); const empty = document.querySelector('.iePlayer-history-empty'); const history = StorageModule.getPlayHistory(); if (history.length === 0) { list.innerHTML = ''; empty.style.display = 'block'; return; } empty.style.display = 'none'; list.innerHTML = history.map(item => ` <div class="iePlayer-history-item"> <div class="iePlayer-history-info"> <div class="iePlayer-history-title">${item.title}</div> <div class="iePlayer-history-meta"><span class="iePlayer-history-source">${item.source}</span><span class="iePlayer-history-episode">${item.episode}</span></div> </div> <div class="iePlayer-history-actions"> <button class="iePlayer-history-play-btn" data-url="${item.url}" data-title="${item.title}" data-source="${item.source}" data-episode="${item.episode}">播放</button> <button class="iePlayer-history-delete-btn" data-id="${item.id}">删除</button> </div> </div>`).join(''); }, }; // ===== 事件管理模块 ===== const EventModule = { initSearchPanel() { const panel = StateModule.get('searchPanel'); if (!panel) return; UIModule.initializeSourceSelector(); panel.addEventListener('click', this.handlePanelClick.bind(this)); panel.querySelector('.iePlayer-search-input').addEventListener('keypress', (e) => { if(e.key === 'Enter') this.performSearch(); }); this.makeDraggable(panel); }, async handlePanelClick(e) { const target = e.target; const panel = StateModule.get('searchPanel'); if (target.matches('.iePlayer-tab-btn')) { panel.querySelector('.iePlayer-tab-btn.active').classList.remove('active'); target.classList.add('active'); panel.querySelector('.iePlayer-tab-content.active').classList.remove('active'); panel.querySelector(`#iePlayer-tab-${target.dataset.tab}`).classList.add('active'); if (target.dataset.tab === 'homepage') await this.loadHomepage(); if (target.dataset.tab === 'history') UIModule.displayPlayHistory(); } else if (target.matches('input[name="iePlayer-searchType"]')) { const isAgg = target.value === 'aggregated'; StateModule.set('isAggregatedSearch', isAgg); StateModule.set('selectedSources', isAgg ? [] : [target.value]); StorageModule.setIsAggregated(isAgg); StorageModule.setSelectedSources(isAgg ? [] : [target.value]); if (panel.querySelector('.iePlayer-tab-btn[data-tab="homepage"].active')) await this.loadHomepage(); } else if (target.matches('.iePlayer-search-btn')) await this.performSearch(); else if (target.id === 'iePlayer-prev-page') this.loadSearchPage(StateModule.get('currentPage') - 1); else if (target.id === 'iePlayer-next-page') this.loadSearchPage(StateModule.get('currentPage') + 1); else if (target.id === 'iePlayer-prev-homepage') await this.loadHomepage(StateModule.get('homepageCurrentPage') - 1); else if (target.id === 'iePlayer-next-homepage') await this.loadHomepage(StateModule.get('homepageCurrentPage') + 1); else if (target.matches('.iePlayer-category-btn')) { const catId = target.dataset.id === 'null' ? null : target.dataset.id; StateModule.set('homepageSelectedCategory', catId); await this.loadHomepage(); } else if (target.matches('.iePlayer-load-sources-btn')) { target.textContent = '加载中...'; const sourcesDiv = target.parentElement; const { videoId, sourceKey } = sourcesDiv.dataset; try { const data = await APIModule.getVideoDetail(videoId, sourceKey); const playUrl = data.list[0].vod_play_url; sourcesDiv.innerHTML = playUrl.split('$$$').map((source, index) => { const name = source.split('$')[0]; return `<button class="iePlayer-play-btn" data-source-index="${index}">${/^[a-zA-Z0-9]+$/.test(name) && name.length < 10 ? name : `线路${index + 1}`}</button>`; }).join(''); } catch { sourcesDiv.innerHTML = '加载失败'; } } else if (target.matches('.iePlayer-play-btn')) { const detailsDiv = target.closest('.iePlayer-video-details'); const episodeList = detailsDiv.querySelector('.iePlayer-episode-list'); const { videoId, sourceKey } = detailsDiv.querySelector('.iePlayer-play-sources').dataset; const sourceIndex = target.dataset.sourceIndex; if(episodeList.dataset.currentIndex === sourceIndex && episodeList.style.display === 'grid') { episodeList.style.display = 'none'; return; } episodeList.innerHTML = '加载中...'; episodeList.style.display = 'grid'; episodeList.dataset.currentIndex = sourceIndex; const data = await APIModule.getVideoDetail(videoId, sourceKey); const playListString = data.list[0].vod_play_url.split('$$$')[sourceIndex]; const episodes = playListString.split(/[#\n\r]+/).filter(ep => ep.trim()); episodeList.innerHTML = episodes.map(ep => { const [name, url] = ep.includes('$') ? ep.split('$', 2) : [ep.split('/').pop().slice(0, 15), ep]; return `<button class="iePlayer-episode-btn" data-url="${url.trim()}" data-title="${data.list[0].vod_name} | ${name}">${name}</button>`; }).join(''); } else if (target.matches('.iePlayer-episode-btn')) { const { url, title } = target.dataset; const sourceName = target.closest('.iePlayer-result-item').querySelector('.iePlayer-source-badge').textContent; PlayerModule.openVideoPlayer(url, title, sourceName, target.textContent); } else if (target.matches('.iePlayer-close-btn')) panel.classList.remove('show'); else if (target.matches('.iePlayer-m3u8-btn')) PlayerModule.openVideoPlayer(panel.querySelector('.iePlayer-m3u8-input').value.trim(), '直接播放'); else if (target.matches('.iePlayer-clear-history-btn')) { StorageModule.clearPlayHistory(); UIModule.displayPlayHistory(); } else if (target.matches('.iePlayer-history-play-btn')) { const { url, title, source, episode } = target.dataset; PlayerModule.openVideoPlayer(url, title, source, episode); } else if (target.matches('.iePlayer-history-delete-btn')) { StorageModule.removePlayHistory(target.dataset.id); UIModule.displayPlayHistory(); } }, async loadHomepage(page = 1) { const panel = StateModule.get('searchPanel'); const tipDiv = panel.querySelector('.iePlayer-homepage-tip'); const resultsDiv = panel.querySelector('.iePlayer-homepage-results'); const loadingDiv = panel.querySelector('.iePlayer-homepage-loading'); const paginationDiv = panel.querySelector('.iePlayer-homepage-pagination'); const categoriesDiv = panel.querySelector('.iePlayer-homepage-categories'); resultsDiv.innerHTML = ''; paginationDiv.style.display = 'none'; tipDiv.innerHTML = ''; if (StateModule.get('isAggregatedSearch')) { tipDiv.textContent = '请选择一个单一视频源以浏览。'; categoriesDiv.innerHTML=''; return; } const sourceKey = StateModule.get('selectedSources')[0]; if (!sourceKey) { tipDiv.textContent = '请先选择一个视频源。'; categoriesDiv.innerHTML=''; return; } loadingDiv.style.display = 'block'; if (!StateModule.get('homepageCategories')[sourceKey] || page === 1) { const categories = await APIModule.fetchCategories(sourceKey); StateModule.get('homepageCategories')[sourceKey] = categories; if(StateModule.get('selectedSources')[0] === sourceKey) { UIModule.displayCategories(categories); } } else { UIModule.displayCategories(StateModule.get('homepageCategories')[sourceKey]); } const selectedCatId = StateModule.get('homepageSelectedCategory'); const data = await APIModule.fetchSourceHomepage(sourceKey, page, selectedCatId); loadingDiv.style.display = 'none'; if (data && data.list && data.list.length > 0) { UIModule.renderVideoList(resultsDiv, data.list); StateModule.set('homepageCurrentPage', data.page); StateModule.set('homepageTotalPages', data.pagecount); paginationDiv.style.display = 'flex'; UIModule.updatePagination('homepage'); } else { resultsDiv.innerHTML = '<div class="iePlayer-no-results">未能加载到内容。</div>'; } }, async performSearch() { const keyword = document.querySelector('.iePlayer-search-input').value.trim(); if (StateModule.get('isSearching')) { StateModule.get('searchController')?.abort(); return; } if (!keyword) return; const controller = new AbortController(); StateModule.set('searchController', controller); StateModule.set('isSearching', true); document.querySelector('.iePlayer-loading').style.display = 'block'; document.querySelector('.iePlayer-results').innerHTML = ''; try { const results = await APIModule.search(StateModule.get('selectedSources')[0], keyword, 1, controller.signal); if (controller.signal.aborted) return; const resultsDiv = document.querySelector('.iePlayer-results'); UIModule.renderVideoList(resultsDiv, results.list); } catch (error) { if (!controller.signal.aborted) document.querySelector('.iePlayer-results').innerHTML = '<div class="iePlayer-no-results">搜索失败或无结果。</div>'; } finally { StateModule.set('isSearching', false); document.querySelector('.iePlayer-loading').style.display = 'none'; } }, makeDraggable(element) { let isDragging = false, initialX, initialY, xOffset = 0, yOffset = 0; const header = element.querySelector('.iePlayer-panel-header'); const dragStart = (e) => { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; if (header.contains(e.target)) isDragging = true; }; const drag = (e) => { if (isDragging) { e.preventDefault(); xOffset = e.clientX - initialX; yOffset = e.clientY - initialY; element.style.transform = `translate3d(${xOffset}px, ${yOffset}px, 0)`; } }; document.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', () => isDragging = false); }, }; // ===== 播放器模块 ===== const PlayerModule = { openVideoPlayer(url, title, source = '', episode = '') { if (!url) { alert('播放地址无效'); return; } StorageModule.addPlayHistory({ title, url, source, episode }); const container = document.createElement('div'); container.className = 'iePlayer-player-container'; container.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 style="width:100%;height:100%;background:#000;" controls autoplay playsinline></video><div class="iePlayer-loading-player">加载中...</div></div>`; document.body.appendChild(container); container.classList.add('show'); const closeHandler = () => this.closeVideoPlayer(container); container.querySelector('.iePlayer-player-close').addEventListener('click', closeHandler); const keyHandler = (e) => { if (e.key === 'Escape') closeHandler(); }; document.addEventListener('keydown', keyHandler); container.keyHandler = keyHandler; this.initVideoPlayer(container, url); }, initVideoPlayer(container, url) { const video = container.querySelector('video'); const loading = container.querySelector('.iePlayer-loading-player'); const loadHls = () => { if (typeof Hls !== 'undefined') { this.setupVideoPlayer(Hls, container, url, video, loading); return; } const script = document.createElement('script'); script.src = ConfigModule.HLS_JS_CDNS[0]; script.onload = () => this.setupVideoPlayer(Hls, container, url, video, loading); script.onerror = () => { loading.textContent = '播放器库加载失败'; }; document.head.appendChild(script); }; if (url.toLowerCase().includes('m3u8')) loadHls(); else { video.src = url; loading.style.display = 'none'; video.play().catch(()=>{}); } }, setupVideoPlayer(Hls, container, url, video, loading) { loading.style.display = 'none'; if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(()=>{})); container.hlsInstance = hls; } else { loading.style.display = 'block'; loading.textContent = '浏览器不支持HLS'; } }, closeVideoPlayer(container) { container.hlsInstance?.destroy(); document.removeEventListener('keydown', container.keyHandler); container.remove(); } }; // ===== 初始化样式 ===== function initStyles() { GM_addStyle(` :root { --panel-width: 420px; --primary-color: #667eea; --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .iePlayer-search-panel { position: fixed; top: 20px; right: 20px; width: var(--panel-width); max-height: 90vh; background: #f8f9fa; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); z-index: 999999; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; display: none; flex-direction: column; } .iePlayer-search-panel.show { display: flex; } .iePlayer-panel-header { background: var(--primary-gradient); color: white; padding: 12px 18px; display: flex; justify-content: space-between; align-items: center; cursor: move; border-radius: 12px 12px 0 0;} .iePlayer-panel-title { font-size: 16px; font-weight: 600; margin: 0; } .iePlayer-close-btn { background: none; border: none; color: white; font-size: 24px; cursor: pointer; opacity: 0.8; transition: opacity 0.2s; } .iePlayer-close-btn:hover { opacity: 1; } .iePlayer-tabs { display: flex; background-color: #f1f3f5; padding: 3px 15px 0; flex-shrink: 0; } .iePlayer-tab-btn { padding: 10px 15px; cursor: pointer; border: none; background: transparent; font-size: 14px; font-weight: 500; color: #868e96; position: relative; } .iePlayer-tab-btn.active { color: var(--primary-color); } .iePlayer-tab-btn.active::after { content: ''; position: absolute; bottom: -1px; left: 0; right: 0; height: 3px; background-color: var(--primary-color); border-radius: 3px 3px 0 0; } .iePlayer-panel-body { padding: 15px; overflow-y: auto; flex-grow: 1; } .iePlayer-tab-content { display: none; } .iePlayer-tab-content.active { display: block; } .iePlayer-section, .iePlayer-m3u8-form { margin-bottom: 15px; padding: 15px; background: #fff; border-radius: 8px; border: 1px solid #e9ecef; } .iePlayer-source-header, .iePlayer-form-header, .iePlayer-m3u8-header, .iePlayer-history-title { font-weight: 600; color: #343a40; font-size: 14px; margin-bottom: 12px; } .iePlayer-source-options { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } .iePlayer-source-option label { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s ease; border: 1px solid #dee2e6; } .iePlayer-source-option input[type="radio"] { accent-color: var(--primary-color); } .iePlayer-source-option label:has(input:checked) { background: #e8eaf6; color: var(--primary-color); border-color: var(--primary-color); } .iePlayer-source-option label:has(input:checked) > span { font-weight: 600; } .iePlayer-search-input, .iePlayer-m3u8-input { width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; margin-bottom: 12px; box-sizing: border-box; } .iePlayer-search-btn, .iePlayer-m3u8-btn, .iePlayer-page-btn, .iePlayer-clear-history-btn { padding: 10px; color: white; border: none; border-radius: 6px; cursor: pointer; background: var(--primary-gradient); font-size: 14px; } .iePlayer-page-btn { background: white; color: var(--primary-color); border: 1px solid var(--primary-color); padding: 8px 12px; font-size: 13px; } .iePlayer-page-btn:disabled { opacity: 0.5; cursor: not-allowed; } .iePlayer-loading, .iePlayer-homepage-loading, .iePlayer-no-results, .iePlayer-homepage-tip { text-align: center; padding: 40px 20px; color: #6c757d; } .iePlayer-results, .iePlayer-homepage-results { display: flex; flex-direction: column; gap: 12px; } .iePlayer-result-item { display: flex; gap: 12px; background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 12px; transition: box-shadow 0.2s; } .iePlayer-result-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.08); } .iePlayer-video-thumb { flex-shrink: 0; width: 80px; height: 110px; border-radius: 4px; background-color: #f1f3f5; } .iePlayer-video-thumb.no-image::before { content: '🖼️'; display: grid; place-items: center; width: 100%; height: 100%; font-size: 30px; color: #adb5bd; } .iePlayer-video-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; } .iePlayer-video-details { flex-grow: 1; display: flex; flex-direction: column; min-width: 0; } .iePlayer-video-header { display: flex; justify-content: space-between; margin-bottom: 4px; } .iePlayer-video-title { font-weight: 600; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .iePlayer-source-badge { font-size: 11px; background: #e9ecef; padding: 2px 6px; border-radius: 4px; flex-shrink: 0; margin-left: 8px; } .iePlayer-video-info { font-size: 12px; color: #6c757d; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .iePlayer-play-sources { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; padding-top: 8px; } .iePlayer-play-btn, .iePlayer-load-sources-btn, .iePlayer-episode-btn { font-size: 12px; padding: 4px 8px; border: 1px solid #dee2e6; background: #f8f9fa; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } .iePlayer-play-btn:hover, .iePlayer-load-sources-btn:hover, .iePlayer-episode-btn:hover { background-color: #e9ecef; } .iePlayer-episode-list { display: none; grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #e9ecef;} .iePlayer-episode-btn { width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .iePlayer-pagination, .iePlayer-homepage-pagination { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; } .iePlayer-homepage-categories { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #e9ecef; } .iePlayer-category-btn { padding: 6px 12px; font-size: 13px; border: 1px solid #dee2e6; background: #fff; border-radius: 16px; cursor: pointer; } .iePlayer-category-btn.active { background: var(--primary-color); color: white; border-color: var(--primary-color); } .iePlayer-history-header { display: flex; justify-content: space-between; align-items: center; } .iePlayer-clear-history-btn { background: #f8f9fa; color: #dc3545; border: 1px solid #dc3545; } .iePlayer-history-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e9ecef; } .iePlayer-history-title { font-weight: 500; } .iePlayer-history-meta { font-size: 12px; color: #6c757d; } .iePlayer-history-play-btn { font-size: 13px; padding: 6px 10px; background: var(--primary-color); color: white; border: none; border-radius: 4px; } .iePlayer-float-btn { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; background: var(--primary-gradient); border-radius: 50%; border: none; color: white; font-size: 24px; cursor: pointer; z-index: 999998; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 20px rgba(0,0,0,0.2); } .iePlayer-player-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); 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; } .iePlayer-player-header { position: absolute; top: 0; left: 0; right: 0; padding: 15px; color: white; } .iePlayer-loading-player { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: white; } `); } // ===== 主初始化函数 ===== function init() { if (window.self !== window.top) return; StateModule.init(); initStyles(); const floatBtn = document.createElement('button'); floatBtn.className = 'iePlayer-float-btn'; floatBtn.textContent = '🎬'; floatBtn.onclick = () => UIModule.toggleSearchPanel(); document.body.appendChild(floatBtn); GM_registerMenuCommand('🎬 打开视频搜索', () => UIModule.toggleSearchPanel()); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();