m3u8Player

支持多源浏览、分类筛选和搜索的M3U8视频播放器,基于HLS.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
    }
})();