// ==UserScript== // @name XyPlayer 便携版 · 云播放聚合 // @namespace https://github.com/xyplayer-portable // @version 1.1.1 // @description 将 XyPlayer 智能解析的"云播放"+"VIP解析"模块移植为纯前端油猴脚本:多资源站并行搜索影视、聚合取剧集、内嵌 ArtPlayer+hls.js 播放 m3u8/mp4、第三方解析线路(iframe)、剧集连播与历史记录。无需 PHP 服务器。 // @author WorkBuddy // @match *://*/* // @exclude *://*.google.com/* // @connect * // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @noframes // @license MIT // ==/UserScript== (function () { 'use strict'; /* ====================================================================== * XyPlayer 便携版 * 原理:复刻 xyplayer-main/upload/video/class.yun.php 的云播放逻辑 * - 搜索: {api}?wd=关键词 → 苹果CMS JSON,list[] 含 vod_id * - 详情: {api}?ac=detail&ids=ID → vod_play_from / vod_play_url * - 分组: vod_play_note(默认$$$) 拆多播放源,# 拆集,名称$地址 拆条目 * - 播放: m3u8 → hls.js,mp4 → 原生 * 优势:GM_xmlhttpRequest 无跨域限制,完全替代 PHP 中转与缓存。 * ==================================================================== */ const SCRIPT_NAME = 'XyPlayer 便携版'; const STORE_SITES = 'xyp_sites_v1'; const STORE_HIST = 'xyp_history_v1'; const STORE_LAST = 'xyp_last_v1'; const STORE_JX = 'xyp_jx_v1'; const MAX_HISTORY = 60; // 经存活验证的默认资源站(苹果CMS 采集API) const DEFAULT_SITES = [ { name: '非凡资源', api: 'http://www.ffzy.tv/api.php/provide/vod', on: true }, { name: '量子资源', api: 'https://cj.lziapi.com/api.php/provide/vod', on: true }, { name: '卧龙资源', api: 'https://collect.wolongzyw.com/api.php/provide/vod', on: true }, ]; // VIP 解析线路(移植 CONFIG["parse"]/jx_url:前缀 + 视频页URL,iframe 加载,2026-08-27 存活验证) const DEFAULT_JX = [ { name: '剧集线路', url: 'https://im1907.top/?jx=', on: true }, { name: '高清线路', url: 'https://jx.m3u8.tv/jiexi/?url=', on: true }, { name: '备用线路', url: 'https://jx.xmflv.com/?url=', on: true }, ]; // 视频平台域名 → 用于自动预填片名 const VIDEO_HOSTS = [ /(^|\.)v\.qq\.com$/i, /(^|\.)m\.v\.qq\.com$/i, /(^|\.)iqiyi\.com$/i, /(^|\.)youku\.com$/i, /(^|\.)mgtv\.com$/i, /(^|\.)sohu\.com$/i, /(^|\.)tv\.sohu\.com$/i, /(^|\.)bilibili\.com$/i, ]; /* ---------------------------- 存储工具 ---------------------------- */ function getSites() { const v = GM_getValue(STORE_SITES); if (Array.isArray(v) && v.length) return v; return JSON.parse(JSON.stringify(DEFAULT_SITES)); } function setSites(list) { GM_setValue(STORE_SITES, list); } function getJx() { const v = GM_getValue(STORE_JX); if (Array.isArray(v) && v.length) return v; return JSON.parse(JSON.stringify(DEFAULT_JX)); } function setJx(list) { GM_setValue(STORE_JX, list); } function getHistory() { try { return JSON.parse(GM_getValue(STORE_HIST) || '[]'); } catch (e) { return []; } } function pushHistory(item) { let h = getHistory().filter(x => !(x.site === item.site && x.id === item.id)); h.unshift(item); if (h.length > MAX_HISTORY) h = h.slice(0, MAX_HISTORY); GM_setValue(STORE_HIST, JSON.stringify(h)); } /* ---------------------------- 网络请求 ---------------------------- */ function gmFetch(url, timeout = 15000) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, timeout, headers: { 'User-Agent': navigator.userAgent, 'Accept': '*/*' }, onload: (res) => { if (res.status >= 200 && res.status < 400) resolve(res.responseText); else reject(new Error('HTTP ' + res.status)); }, onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), }); }); } async function searchSite(site, keyword, page = 1) { const url = site.api + '?wd=' + encodeURIComponent(keyword) + (page > 1 ? '&pg=' + page : ''); const text = await gmFetch(url); const json = JSON.parse(text); const list = Array.isArray(json.list) ? json.list : []; return list.map(v => ({ site: site.name, api: site.api, id: v.vod_id, title: v.vod_name || '未知', type: v.type_name || v.class || '', remarks: v.vod_remarks || '', pic: v.vod_pic || '', })).filter(v => v.id !== undefined && v.id !== ''); } async function fetchDetail(api, id) { const url = api.replace(/\/$/, '') + '?ac=detail&ids=' + encodeURIComponent(id); const text = await gmFetch(url); const json = JSON.parse(text); if (!json.list || !json.list.length) throw new Error('未找到剧集数据'); const v = json.list[0]; // 复刻 class.yun.php::getvideobyid 的分组解析 const note = v.vod_play_note || '$$$'; const froms = String(v.vod_play_from || '').split(note).filter(Boolean); const urls = String(v.vod_play_url || '').split(note); const groups = froms.map((flag, i) => { const eps = String(urls[i] || '').split('#').filter(Boolean).map(seg => { const parts = seg.split('$'); return { name: parts[0] || ('第' + (i + 1) + '集'), url: (parts[1] || '').trim() }; }).filter(e => e.url && /^https?:\/\//i.test(e.url)); return { flag, episodes: eps }; }).filter(g => g.episodes.length); return { title: v.vod_name || '未知', pic: v.vod_pic || '', year: v.vod_year || '', actor: v.vod_actor || '', content: (v.vod_content || '').replace(/<[^>]+>/g, '').trim(), groups, }; } /* ---------------------------- 播放器加载 ---------------------------- */ const CDN = { hls: 'https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js', art: 'https://cdn.jsdelivr.net/npm/artplayer@5.2.3/dist/artplayer.js', }; const loaded = {}; function loadScript(src) { if (loaded[src] !== undefined) return loaded[src]; loaded[src] = new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.async = true; s.onload = () => resolve(true); s.onerror = () => reject(new Error('CDN 加载失败: ' + src)); (document.head || document.documentElement).appendChild(s); }); return loaded[src]; } function detectType(url) { const clean = url.split('#')[0].split('?')[0].toLowerCase(); if (clean.endsWith('.m3u8')) return 'hls'; if (/\.(mp4|webm|ogg)$/.test(clean)) return 'video'; return 'hls'; // 资源站基本都是 m3u8 直链,兜底按 hls 处理 } // 在指定容器内创建 ArtPlayer(hls/mp4 自适应),成功返回 true async function mountArtPlayer(container, url, title) { container.style.position = 'relative'; const holder = el('div'); holder.style.cssText = 'position:absolute;inset:0;'; container.appendChild(holder); try { await loadScript(CDN.hls); await loadScript(CDN.art); } catch (e) { container.appendChild(el('div', 'status', '播放器组件加载失败(CDN 被墙或断网),请重试或检查网络')); return false; } if (typeof Artplayer === 'undefined') { container.appendChild(el('div', 'status', '播放器组件加载异常')); return false; } const option = { container: holder, url, autoplay: true, autoSize: true, setting: true, hotkey: true, pip: true, mutex: true, theme: '#ff5f6d', title, moreVideoAttr: { crossOrigin: 'anonymous' }, }; if (detectType(url) === 'hls' && window.Hls && window.Hls.isSupported()) { option.customType = { m3u8: function (video, src, art) { const hls = new Hls({ maxBufferLength: 30 }); hls.loadSource(src); hls.attachMedia(video); // 默认最高画质(复刻用户"调到4K"需求) hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => { if (data.levels && data.levels.length > 1) { hls.currentLevel = data.levels.length - 1; } }); art.hls = hls; art.on('destroy', () => { try { hls.destroy(); } catch (e) { } }); }, }; } try { ui.art = new Artplayer(option); } catch (e) { container.appendChild(el('div', 'status', '初始化播放器失败:' + (e.message || e))); return false; } return true; } /* ---------------------------- 页面标题抓取 ---------------------------- */ function isVideoHost() { try { return VIDEO_HOSTS.some(re => re.test(location.hostname)); } catch (e) { return false; } } function guessTitleFromPage() { let t = document.title || ''; t = t.replace(/[_\-|-–]\s*(爱奇艺|腾讯视频|优酷|芒果TV|搜狐视频|哔哩哔哩|bilibili|TV版).*$/i, '') .replace(/[((]?\s*(高清|免费|完整|在线观看|全集|正片|独播)[^))]*[))]?/g, '') .replace(/第\s*\d+\s*[集话回期].*$/, '') .replace(/[_\-|]\s*$/, '') .trim(); // 剧集页: 尝试 h1 const h1 = document.querySelector('h1'); if (h1 && h1.textContent.trim().length >= 2 && t.length < 2) t = h1.textContent.trim(); return t; } /* ====================================================================== * UI —— Shadow DOM 隔离,避免被站点样式污染 * ==================================================================== */ let ui = null; // { host, shadow, root, art } const CSS = ` :host { all: initial; } * { box-sizing: border-box; margin: 0; padding: 0; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; } .fab { position: fixed; right: 22px; bottom: 88px; z-index: 2147483600; width: 46px; height: 46px; border-radius: 50%; border: none; cursor: pointer; background: linear-gradient(135deg, #ff5f6d, #ffc371); color: #fff; font-size: 13px; font-weight: 700; box-shadow: 0 4px 14px rgba(255,95,109,.45); transition: transform .15s; } .fab:hover { transform: scale(1.1); } .mask { position: fixed; inset: 0; z-index: 2147483601; background: rgba(10,12,18,.72); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; } .panel { width: min(1060px, 94vw); height: min(720px, 90vh); background: #16181f; border-radius: 14px; color: #e7eaf0; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 18px 60px rgba(0,0,0,.55); } .topbar { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid #262a35; flex-shrink: 0; } .logo { font-size: 15px; font-weight: 700; color: #ff8a9b; white-space: nowrap; } .searchbox { flex: 1; display: flex; gap: 8px; } .searchbox input { flex: 1; height: 34px; border: 1px solid #2c313d; border-radius: 8px; background: #0f1117; color: #e7eaf0; padding: 0 12px; font-size: 14px; outline: none; } .searchbox input:focus { border-color: #ff5f6d; } .btn { height: 34px; padding: 0 14px; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; color: #fff; background: #2c313d; white-space: nowrap; } .btn:hover { background: #3a4150; } .btn.primary { background: linear-gradient(135deg, #ff5f6d, #ff8a5f); } .btn.primary:hover { filter: brightness(1.1); } .btn.small { height: 28px; padding: 0 10px; font-size: 12px; } .icon-btn { width: 30px; height: 30px; border: none; border-radius: 8px; cursor: pointer; background: transparent; color: #8b93a5; font-size: 16px; } .icon-btn:hover { background: #262a35; color: #fff; } .body { flex: 1; overflow: auto; padding: 14px 16px; } .body::-webkit-scrollbar { width: 8px; } .body::-webkit-scrollbar-thumb { background: #2c313d; border-radius: 4px; } .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; } .card { display: flex; flex-direction: column; gap: 4px; padding: 12px; background: #1d2029; border: 1px solid #262a35; border-radius: 10px; cursor: pointer; transition: border-color .15s, transform .15s; } .card:hover { border-color: #ff5f6d; transform: translateY(-2px); } .card .t { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .card .meta { font-size: 12px; color: #8b93a5; display: flex; gap: 8px; } .card .site-tag { display: inline-block; font-size: 11px; color: #ff8a9b; background: rgba(255,95,109,.12); border-radius: 4px; padding: 1px 6px; } .toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; } .toolbar .hint { font-size: 12px; color: #8b93a5; } .chips { display: flex; gap: 6px; flex-wrap: wrap; } .chip { height: 26px; padding: 0 12px; border-radius: 13px; border: 1px solid #2c313d; background: transparent; color: #8b93a5; font-size: 12px; cursor: pointer; } .chip.on { background: rgba(255,95,109,.15); border-color: #ff5f6d; color: #ff8a9b; } .status { padding: 40px; text-align: center; color: #8b93a5; font-size: 13px; } .spinner { width: 26px; height: 26px; margin: 0 auto 12px; border: 3px solid #2c313d; border-top-color: #ff5f6d; border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* 播放视图 */ .player-wrap { display: flex; flex-direction: column; height: 100%; gap: 12px; } .player-main { flex: 1; min-height: 0; display: flex; gap: 12px; } .player-left { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; } .player-box { flex: 1; min-height: 0; background: #000; border-radius: 10px; overflow: hidden; position: relative; } .player-title { font-size: 15px; font-weight: 700; margin-bottom: 8px; } .player-title .sub { font-size: 12px; color: #8b93a5; font-weight: 400; margin-left: 8px; } .eps-panel { width: 260px; flex-shrink: 0; background: #1d2029; border-radius: 10px; display: flex; flex-direction: column; overflow: hidden; } .eps-panel .hd { padding: 10px 12px; font-size: 13px; font-weight: 600; border-bottom: 1px solid #262a35; display: flex; justify-content: space-between; align-items: center; } .eps-list { flex: 1; overflow: auto; padding: 8px; display: flex; flex-wrap: wrap; gap: 6px; align-content: flex-start; } .eps-list::-webkit-scrollbar { width: 6px; } .eps-list::-webkit-scrollbar-thumb { background: #2c313d; border-radius: 3px; } .ep { min-width: 42px; height: 30px; padding: 0 8px; border-radius: 6px; border: 1px solid #2c313d; background: transparent; color: #c3c9d6; font-size: 12px; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ep:hover { border-color: #ff5f6d; } .ep.active { background: linear-gradient(135deg, #ff5f6d, #ff8a5f); border-color: transparent; color: #fff; } .intro { font-size: 12px; color: #8b93a5; line-height: 1.7; margin-top: 10px; max-height: 66px; overflow: auto; } /* 设置视图 */ .site-row { display: flex; gap: 8px; margin-bottom: 8px; } .site-row input { flex: 1; height: 32px; border: 1px solid #2c313d; border-radius: 8px; background: #0f1117; color: #e7eaf0; padding: 0 10px; font-size: 13px; } .site-row .nm { max-width: 130px; } .msg { position: fixed; left: 50%; top: 18px; transform: translateX(-50%); background: #23262f; color: #e7eaf0; font-size: 13px; padding: 9px 18px; border-radius: 8px; z-index: 2147483660; box-shadow: 0 6px 20px rgba(0,0,0,.4); } .hist-item { display:flex; justify-content: space-between; align-items:center; padding: 9px 12px; background:#1d2029; border:1px solid #262a35; border-radius:8px; margin-bottom:6px; cursor:pointer; } .hist-item:hover { border-color:#ff5f6d; } .hist-item .t { font-size:13px; } .hist-item .m { font-size:11px; color:#8b93a5; margin-top:2px; } .hist-item .go { color:#ff8a9b; font-size:12px; white-space:nowrap; } `; function el(tag, cls, text) { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; } function toast(msg, ms = 2200) { if (!ui) return; const t = el('div', 'msg', msg); ui.shadow.appendChild(t); setTimeout(() => t.remove(), ms); } /* ---------------------------- UI 主体 ---------------------------- */ function initUI() { if (ui) return; const host = document.createElement('div'); host.id = 'xyp-portable-host'; const shadow = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = CSS; shadow.appendChild(style); const fab = el('button', 'fab', '▶ 播'); fab.title = SCRIPT_NAME + ' (Ctrl+Shift+X)'; fab.addEventListener('click', openPanel); shadow.appendChild(fab); document.documentElement.appendChild(host); ui = { host, shadow, art: null, view: null }; document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.code === 'KeyX') { e.preventDefault(); openPanel(); } }, true); } function openPanel() { initUI(); if (ui.view) { ui.view.style.display = 'flex'; return; } const mask = el('div', 'mask'); const panel = el('div', 'panel'); mask.addEventListener('click', (e) => { if (e.target === mask) hidePanel(); }); mask.appendChild(panel); const topbar = el('div', 'topbar'); const logo = el('div', 'logo', '▶ XyPlayer 便携版'); const searchbox = el('div', 'searchbox'); const input = el('input'); input.placeholder = '输入影视名称搜索资源站…'; input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(input.value.trim()); }); const btnSearch = el('button', 'btn primary', '搜索'); btnSearch.addEventListener('click', () => doSearch(input.value.trim())); const btnHist = el('button', 'btn', '历史'); btnHist.addEventListener('click', renderHistory); const btnSet = el('button', 'btn', '资源站'); btnSet.addEventListener('click', renderSettings); const btnJx = el('button', 'btn', 'VIP解析'); btnJx.addEventListener('click', () => renderJx()); const btnBack = el('button', 'btn', '返回搜索'); btnBack.addEventListener('click', () => { destroyArt(); renderSearchHome(); }); const btnClose = el('button', 'icon-btn', '✕'); btnClose.addEventListener('click', hidePanel); searchbox.append(input, btnSearch); topbar.append(logo, searchbox, btnHist, btnSet, btnJx, btnBack, btnClose); const body = el('div', 'body'); panel.append(topbar, body); ui.shadow.appendChild(mask); ui.view = mask; ui.$ = { input, body }; // 视频站自动预填标题 if (isVideoHost()) { const t = guessTitleFromPage(); if (t && t.length >= 2) { input.value = t; toast('已识别本页片名:' + t); } } renderSearchHome(); } function hidePanel() { if (ui && ui.view) ui.view.style.display = 'none'; } function setBody(node) { destroyArt(); ui.$.body.innerHTML = ''; if (node) ui.$.body.appendChild(node); } /* ---------------------------- 视图:搜索首页 ---------------------------- */ function renderSearchHome() { const box = el('div'); const hist = getHistory(); if (hist.length) { box.appendChild(el('div', 'toolbar', '')).append(el('span', 'hint', '继续观看')); const g = el('div', 'grid'); hist.filter(h => h.api).slice(0, 6).forEach(h => { const c = el('div', 'card'); c.appendChild(el('div', 't', h.title)); const meta = el('div', 'meta'); meta.append(el('span', 'site-tag', h.site)); meta.appendChild(el('span', null, h.epName ? '看到 ' + h.epName : '')); c.append(meta); c.addEventListener('click', () => playById(h.api, h.id, h.site, h.title, h.epIndex || 0, h.groupIndex || 0)); g.appendChild(c); }); box.appendChild(g); } else { box.appendChild(el('div', 'status', '输入名称开始搜索 · 支持多资源站聚合')); } setBody(box); } /* ---------------------------- 视图:搜索结果 ---------------------------- */ async function doSearch(keyword) { if (!keyword) { toast('请输入关键词'); return; } const sites = getSites().filter(s => s.on); if (!sites.length) { toast('没有启用的资源站'); return; } const loading = el('div', 'status'); const sp = el('div', 'spinner'); loading.append(sp, el('div', null, '正在并行搜索 ' + sites.length + ' 个资源站…')); setBody(loading); const settled = await Promise.allSettled(sites.map(s => searchSite(s, keyword))); const ok = [], fail = []; settled.forEach((r, i) => { if (r.status === 'fulfilled') ok.push(...r.value); else fail.push(sites[i].name + ': ' + (r.reason && r.reason.message || '失败')); }); const box = el('div'); // 精确匹配优先 ok.sort((a, b) => { const ea = a.title === keyword ? 0 : (a.title.includes(keyword) ? 1 : 2); const eb = b.title === keyword ? 0 : (b.title.includes(keyword) ? 1 : 2); return ea - eb; }); if (fail.length) { const hint = el('div', 'toolbar'); hint.appendChild(el('span', 'hint', '⚠ 部分站点失败:' + fail.join(';'))); box.appendChild(hint); } if (!ok.length) { box.appendChild(el('div', 'status', '没有找到 "' + keyword + '" 的资源,换个关键词试试')); } else { const toolbar = el('div', 'toolbar'); toolbar.appendChild(el('span', 'hint', '共 ' + ok.length + ' 条结果(点击卡片选集播放)')); box.appendChild(toolbar); const g = el('div', 'grid'); const seen = new Set(); ok.forEach(v => { const key = v.site + v.id; if (seen.has(key)) return; seen.add(key); const c = el('div', 'card'); c.appendChild(el('div', 't', v.title)); const meta = el('div', 'meta'); meta.append(el('span', 'site-tag', v.site)); if (v.type) meta.appendChild(el('span', null, v.type)); if (v.remarks) meta.appendChild(el('span', null, v.remarks)); c.append(meta); c.addEventListener('click', () => playById(v.api, v.id, v.site, v.title, 0, 0)); g.appendChild(c); }); box.appendChild(g); } setBody(box); } /* ---------------------------- 视图:播放 ---------------------------- */ async function playById(api, id, site, title, epIndex = 0, groupIndex = 0) { const loading = el('div', 'status'); loading.append(el('div', 'spinner'), el('div', null, '正在获取剧集:' + title + ' …')); setBody(loading); try { const d = await fetchDetail(api, id); renderPlayer({ api, id, site, ...d }, groupIndex, epIndex); } catch (e) { const box = el('div', 'status', '获取剧集失败:' + (e.message || e) + '(该站可能失效或被拦截,试试其他来源)'); setBody(box); } } function renderPlayer(meta, groupIndex, epIndex) { const wrap = el('div', 'player-wrap'); const main = el('div', 'player-main'); const left = el('div', 'player-left'); const titleBar = el('div', 'player-title'); titleBar.appendChild(el('span', null, meta.title)); titleBar.appendChild(el('span', 'sub', meta.year ? meta.year + ' · ' + meta.site : meta.site)); const pbox = el('div', 'player-box'); left.append(titleBar, pbox); const right = el('div', 'eps-panel'); const hd = el('div', 'hd'); hd.appendChild(el('span', null, '播放列表')); const chips = el('div', 'chips'); meta.groups.forEach((g, i) => { const chip = el('button', 'chip' + (i === groupIndex ? ' on' : ''), g.flag + ' (' + g.episodes.length + ')'); chip.title = '播放源:' + g.flag; chip.addEventListener('click', () => { destroyArt(); renderPlayer(meta, i, 0); }); chips.appendChild(chip); }); hd.appendChild(chips); const intro = el('div', 'intro', meta.content ? meta.content.slice(0, 300) : ''); intro.style.cssText = 'margin-top:0;padding:6px 12px;border-bottom:1px solid #262a35;max-height:90px;'; const epsList = el('div', 'eps-list'); right.append(hd, intro, epsList); main.append(left, right); wrap.appendChild(main); setBody(wrap); const groups = meta.groups; if (!groups.length) { pbox.appendChild(el('div', 'status', '无可播放的剧集')); return; } const group = groups[Math.min(groupIndex, groups.length - 1)]; const eps = group.episodes; function buildEps(cur) { epsList.innerHTML = ''; eps.forEach((e, i) => { const b = el('button', 'ep' + (i === cur ? ' active' : ''), e.name); b.title = e.name + ' · ' + e.url.slice(0, 80); b.addEventListener('click', () => startPlay(i)); epsList.appendChild(b); }); } async function startPlay(index) { const ep = eps[index]; buildEps(index); pushHistory({ site: meta.site, api: meta.api, id: meta.id, title: meta.title, epIndex: index, epName: ep.name, groupIndex, time: Date.now(), }); GM_setValue(STORE_LAST, JSON.stringify({ api: meta.api, id: meta.id, site: meta.site, title: meta.title, epIndex: index, groupIndex })); destroyArt(); pbox.innerHTML = ''; const ok = await mountArtPlayer(pbox, ep.url, meta.title + ' · ' + ep.name); if (!ok) return; // 连播:播完自动下一集(复刻 xyplayer 剧集连播) ui.art.on('video:ended', () => { if (index + 1 < eps.length) { toast('自动播放下一集:' + eps[index + 1].name); startPlay(index + 1); } else { toast('已播完最后一集'); } }); } buildEps(Math.min(epIndex, eps.length - 1)); startPlay(Math.min(epIndex, eps.length - 1)); } function destroyArt() { if (ui && ui.art) { try { ui.art.destroy(true); } catch (e) { } ui.art = null; } } /* ---------------------------- 视图:VIP 解析 ---------------------------- */ // 复刻 jx.php + CONFIG["parse"]/jx_url: // 视频文件直链(.m3u8/.mp4…) → 内置 ArtPlayer 播放 // 视频页地址(腾讯/爱奇艺/优酷/芒果…) → 前缀线路 + URL 拼 iframe 播放 function renderJx(prefillUrl) { const lines = getJx().filter(l => l.on); const wrap = el('div', 'player-wrap'); const main = el('div', 'player-main'); const left = el('div', 'player-left'); const titleBar = el('div', 'player-title'); titleBar.appendChild(el('span', null, 'VIP 解析播放')); titleBar.appendChild(el('span', 'sub', '粘贴视频页地址或直链,选择线路播放')); const inputRow = el('div', 'searchbox'); inputRow.style.margin = '0 0 10px'; const input = el('input'); input.placeholder = 'https://v.qq.com/x/cover/… 或 .m3u8 / .mp4 直链'; input.value = prefillUrl || (isVideoHost() ? location.href : ''); input.addEventListener('keydown', e => { if (e.key === 'Enter') startJx(input.value.trim(), 0); }); const go = el('button', 'btn primary', '播放'); go.addEventListener('click', () => startJx(input.value.trim(), 0)); inputRow.append(input, go); const chips = el('div', 'chips'); chips.style.margin = '0 0 10px'; const pbox = el('div', 'player-box'); const tip = el('div', 'intro', '解析线路为第三方网页播放器(iframe 加载),效果取决于线路本身,失败请切换其他线路;' + '视频直链会自动改用内置播放器。在腾讯/爱奇艺等视频页打开时可自动填入当前页地址。'); left.append(titleBar, inputRow, chips, pbox, tip); main.appendChild(left); wrap.appendChild(main); setBody(wrap); if (!lines.length) { pbox.appendChild(el('div', 'status', '没有启用的解析线路,请在「资源站」设置中添加')); return; } function buildChips(cur) { chips.innerHTML = ''; lines.forEach((l, i) => { const c = el('button', 'chip' + (i === cur ? ' on' : ''), l.name); c.title = l.url; c.addEventListener('click', () => startJx(input.value.trim(), i)); chips.appendChild(c); }); } buildChips(-1); async function startJx(url, lineIdx) { if (!/^https?:\/\//i.test(url)) { toast('请输入视频页地址或直链'); return; } const idx = Math.max(0, Math.min(lineIdx, lines.length - 1)); const line = lines[idx]; buildChips(idx); destroyArt(); pbox.innerHTML = ''; // 分流(复刻 jx.php):视频文件直链 → 内置播放器 if (/\.(ogg|mp4|webm|m3u8)([?#]|$)/i.test(url)) { await mountArtPlayer(pbox, url, '直链播放'); return; } // 视频页 → 第三方解析线路 iframe(与原程序 urlplay(api+url) 一致,URL 不编码) const frame = document.createElement('iframe'); frame.src = line.url + url; frame.style.cssText = 'width:100%;height:100%;border:0;display:block;'; frame.setAttribute('allowfullscreen', 'true'); frame.setAttribute('scrolling', 'no'); frame.setAttribute('referrerpolicy', 'no-referrer'); pbox.appendChild(frame); pushHistory({ type: 'jx', url, site: line.name, title: url, time: Date.now() }); } } /* ---------------------------- 视图:历史 ---------------------------- */ function renderHistory() { const hist = getHistory(); const box = el('div'); if (!hist.length) { box.appendChild(el('div', 'status', '暂无播放历史')); setBody(box); return; } const toolbar = el('div', 'toolbar'); toolbar.appendChild(el('span', 'hint', '播放历史(' + hist.length + ')')); const clear = el('button', 'btn small', '清空历史'); clear.addEventListener('click', () => { GM_setValue(STORE_HIST, '[]'); renderHistory(); }); toolbar.appendChild(clear); box.appendChild(toolbar); hist.forEach(h => { const it = el('div', 'hist-item'); const l = el('div'); l.appendChild(el('div', 't', h.title)); l.appendChild(el('div', 'm', h.site + (h.epName ? ' · 看到 ' + h.epName : '') + (h.type === 'jx' ? ' · 解析' : '') + ' · ' + new Date(h.time).toLocaleString())); const r = el('div', 'go', '继续 ▶'); it.append(l, r); it.addEventListener('click', () => { if (h.type === 'jx') renderJx(h.url); else playById(h.api, h.id, h.site, h.title, h.epIndex || 0, h.groupIndex || 0); }); box.appendChild(it); }); setBody(box); } /* ---------------------------- 视图:资源站设置 ---------------------------- */ function renderSettings() { const sites = getSites(); const box = el('div'); const toolbar = el('div', 'toolbar'); toolbar.appendChild(el('span', 'hint', '资源站格式:苹果CMS 采集API(…/api.php/provide/vod),增删后即时生效')); const reset = el('button', 'btn small', '恢复默认'); reset.addEventListener('click', () => { setSites(JSON.parse(JSON.stringify(DEFAULT_SITES))); setJx(JSON.parse(JSON.stringify(DEFAULT_JX))); renderSettings(); }); toolbar.appendChild(reset); box.appendChild(toolbar); const rows = []; function addRow(s = { name: '', api: '', on: true }) { const row = el('div', 'site-row'); const nm = el('input', 'nm'); nm.value = s.name; nm.placeholder = '名称'; const api = el('input'); api.value = s.api; api.placeholder = 'https://…/api.php/provide/vod'; const toggle = el('button', 'btn small', s.on ? '已启用' : '已停用'); toggle.addEventListener('click', () => { s.on = !s.on; toggle.textContent = s.on ? '已启用' : '已停用'; }); const del = el('button', 'btn small', '删除'); del.addEventListener('click', () => { row.remove(); rows.splice(rows.indexOf(r), 1); }); row.append(nm, api, toggle, del); box.appendChild(row); const r = { row, nm, api, get s() { return { name: nm.value.trim(), api: api.value.trim().replace(/\/$/, ''), on: s.on }; } }; rows.push(r); } sites.forEach(addRow); const addBtn = el('button', 'btn small', '+ 添加资源站'); addBtn.addEventListener('click', () => addRow()); box.appendChild(addBtn); // ---- 解析线路管理(移植 CONFIG["parse"] / jx_url)---- const jxHead = el('div', 'toolbar'); jxHead.style.marginTop = '16px'; jxHead.appendChild(el('span', 'hint', 'VIP 解析线路:前缀 + 视频页URL 拼 iframe 播放,如 https://jx.xxx.com/?url=')); box.appendChild(jxHead); const jxRows = []; function addJxRow(s = { name: '', url: '', on: true }) { const row = el('div', 'site-row'); const nm = el('input', 'nm'); nm.value = s.name; nm.placeholder = '线路名称'; const url = el('input'); url.value = s.url; url.placeholder = 'https://jx.xxx.com/?url='; const toggle = el('button', 'btn small', s.on ? '已启用' : '已停用'); toggle.addEventListener('click', () => { s.on = !s.on; toggle.textContent = s.on ? '已启用' : '已停用'; }); const del = el('button', 'btn small', '删除'); del.addEventListener('click', () => { row.remove(); jxRows.splice(jxRows.indexOf(r), 1); }); row.append(nm, url, toggle, del); box.appendChild(row); const r = { nm, url, get s() { return { name: nm.value.trim(), url: url.value.trim(), on: !!s.on }; } }; jxRows.push(r); } getJx().forEach(addJxRow); const addJxBtn = el('button', 'btn small', '+ 添加解析线路'); addJxBtn.addEventListener('click', () => addJxRow()); box.appendChild(addJxBtn); const save = el('button', 'btn primary', '保存'); save.style.marginTop = '10px'; save.addEventListener('click', () => { const list = rows.map(r => r.s).filter(s => s.name && s.api); if (!list.length) { toast('至少保留一个资源站'); return; } setSites(list); const jl = jxRows.map(r => r.s).filter(s => s.name && s.url); setJx(jl); toast('已保存 ' + list.length + ' 个资源站 / ' + jl.length + ' 条解析线路'); renderSearchHome(); }); box.appendChild(save); setBody(box); } /* ---------------------------- 启动 ---------------------------- */ function boot() { // 只在顶层文档注入悬浮球 if (window.top !== window.self) return; if (document.contentType && !/html|xml/i.test(document.contentType)) return; initUI(); } GM_registerMenuCommand('打开云播放面板 (Ctrl+Shift+X)', openPanel); GM_registerMenuCommand('VIP 解析播放', () => { openPanel(); renderJx(); }); GM_registerMenuCommand('资源站管理', () => { openPanel(); renderSettings(); }); GM_registerMenuCommand('继续上次播放', () => { openPanel(); try { const last = JSON.parse(GM_getValue(STORE_LAST) || 'null'); if (last && last.api) playById(last.api, last.id, last.site, last.title, last.epIndex || 0, last.groupIndex || 0); else toast('没有上次播放记录'); } catch (e) { toast('记录读取失败'); } }); boot(); })();