// ==UserScript== // @name External Player // @name:zh-CN 外部播放器 // @namespace https://github.com/LuckyPuppy514/external-player // @copyright 2024, Grant LuckyPuppy514 (https://github.com/LuckyPuppy514) // @version 1.0.0 // @license MIT // @description Play web video via external player // @description:zh-CN 使用外部播放器播放网页中的视频 // @icon https://www.lckp.top/gh/LuckyPuppy514/pic-bed/common/mpv.png // @author LuckyPuppy514 // @homepage https://github.com/LuckyPuppy514/external-player // @include *://* // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @require https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/pako/2.0.4/pako.min.js // @downloadURL none // ==/UserScript== 'use strict'; const SETTING_URL = undefined; // const SETTING_URL = 'http://127.0.0.1:5500/setting.html'; const VIDEO_URL_REGEX_GLOBAL = /https?:\/\/((?![^"^']*http)[^"^']+(\.|%2e)(mp4|mkv|flv|m3u8|m4s|m3u|mov|avi|wmv|webm)(\?[^"^']+|))|((?![^"^']*http)[^"^']+\?[^"^']+(\.|%2e|video_)(mp4|mkv|flv|mov|avi|wmv|webm|m3u8|m3u)[^"^']*)/ig; const VIDEO_URL_REGEX_EXACT = /^https?:\/\/((?![^"^']*http)[^"^']+(\.|%2e)(mp4|mkv|flv|m3u8|m4s|m3u|mov|avi|wmv|webm)(\?[^"^']+|))|((?![^"^']*http)[^"^']+\?[^"^']+(\.|%2e|video_)(mp4|mkv|flv|mov|avi|wmv|webm|m3u8|m3u)[^"^']*)$/ig; const defaultConfig = { global: { version: '1.0.0', language: (navigator.language || navigator.userLanguage) === 'zh-CN' ? 'zh' : 'en', buttonXCoord: '0', buttonYCoord: '0', buttonScale: '1.00', buttonVisibilityDuration: '5000', networkProxy: '', parser: { ytdlp: { regex: [ "https://www.youtube.com/watch\\?.+", "https://www.youtube.com/playlist\\?list=.+", "https://www.lckp.top/play-with-mpv/index.html", ], preferredQuality: 'unlimited', }, video: { regex: [ "https://www.libvio.fun/play/.+", "https://www.tucao.my/play/.+", "https://ddys.pro/.+" ] }, url: { regex: [ "https://anime.girigirilove.com/play.+", ] }, html: { regex: [] }, script: { regex: [] }, request: { regex: [] }, bilibili: { regex: [ "https://www.bilibili.com/bangumi/play/.+", "https://www.bilibili.com/video/.+", "https://www.bilibili.com/list/.+", "https://www.bilibili.com/festival/.+" ], preferredQuality: '127', preferredSubtitle: 'off', preferredCodec: '12', }, bilibiliLive: { regex: [ "https://live.bilibili.com/\\d+.*", "https://live.bilibili.com/blanc/\\d+.*", "https://live.bilibili.com/blackboard/era/.+", ], preferredQuality: '4', preferredLine: '0', } } }, players: [{ name: 'IINA', system: 'mac', icon: 'https://upload.wikimedia.org/wikipedia/commons/5/51/IINA_Logo.png', iconSize: 60, playEvent: "const delimiter = '&';\n\nlet args = [\n `url=${encodeURIComponent(media.video)}`,\n media.origin ? `mpv_http-header-fields=${encodeURIComponent('origin: ' + media.origin)}` : '',\n media.referer ? `mpv_http-header-fields=${encodeURIComponent('referer: ' + media.referer)}` : '',\n]\nargs = args.filter(item => item !== '');\n\nconsole.log(args);\n\nwindow.open(`iina://weblink?${args.join(delimiter)}`, '_self');", presetEvent: { playAuto: false, pauseAuto: true, closeAuto: false }, enable: true, readonly: true, }, { name: 'PotPlayer', system: 'windows', icon: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PotPlayer_logo_%282017%29.png', iconSize: 50, playEvent: "let args = [\n `\"${media.video}\"`,\n media.subtitle ? `/sub=\"${media.subtitle}\"` : '',\n media.origin ? `/headers=\"origin: ${media.origin}\"` : '',\n media.referer ? `/referer=\"${media.referer}\"` : '',\n config.networkProxy ? `/user_agent=\"${config.networkProxy}\"` : '',\n media.title ? `/title=\"${media.title}\"` : '',\n]\nargs = args.filter(item => item !== '');\n\nconsole.log(args);\n\nwindow.open(`ush://${player.name}?${compress(args.join(' '))}`, '_self');", presetEvent: { playAuto: false, pauseAuto: true, closeAuto: false }, enable: true, readonly: true, }, { name: 'MPV', system: 'windows', icon: 'https://upload.wikimedia.org/wikipedia/commons/6/66/Unofficial_Mpv_logo_%28with_gradients%29.svg', iconSize: 55, playEvent: "let args = [\n `\"${media.video}\"`,\n media.audio ? `--audio-file=\"${media.audio}\"` : '',\n media.subtitle ? `--sub-file=\"${media.subtitle}\"` : '',\n media.origin ? `--http-header-fields=\"origin: ${media.origin}\"` : '',\n media.referer ? `--http-header-fields=\"referer: ${media.referer}\"` : '',\n config.networkProxy ? `--http-proxy=\"${config.networkProxy}\"` : '',\n media.ytdlp.networkProxy ? `--ytdl-raw-options=\"proxy=[${media.ytdlp.networkProxy}]\"` : '',\n media.ytdlp.quality ? `--ytdl-format=\"bestvideo[height<=?${media.ytdlp.quality}]%2Bbestaudio/best\"` : '',\n media.bilibili.cid ? `--script-opts-append=\"cid=${media.bilibili.cid}\"` : '',\n media.title ? `--force-media-title=\"${media.title}\"` : '',\n]\nargs = args.filter(item => item !== '');\n\nconsole.log(args);\n\nwindow.open(`ush://${player.name}?${compress(args.join(' '))}`, '_self');", presetEvent: { playAuto: false, pauseAuto: true, closeAuto: false }, enable: true, readonly: true, } ] } const translations = { en: { loadSuccessfully: 'Load successfully', loadTimeout: 'Load timeout ......', saveSuccessfully: 'Save successfully', loadFail: 'Load fail', requireLoginOrVip: 'Require login or vip', noMatchingParserFound: 'No matching parser found', onlyNewTabsCanCloseAutomatically: 'Only new tabs can close automatically' }, zh: { loadSuccessfully: '加载成功', loadTimeout: '加载超时 ......', saveSuccessfully: '保存成功', loadFail: '加载失败', requireLoginOrVip: '需要登录或会员', noMatchingParserFound: '没有匹配的解析器', onlyNewTabsCanCloseAutomatically: '只有新标签页才能自动关闭' } }; const REFRESH_INTERVAL = 500; const MAX_TRY_COUNT = 5; var currentTryCount; var currentConfig; var currentUrl; var currentParser; var currentMedia; var currentPlayer; var translation; class BaseParser { constructor() { currentMedia = { video: undefined, audio: undefined, subtitle: undefined, title: undefined, origin: undefined, referer: undefined, bilibili: { cid: undefined }, ytdlp: { quality: undefined, networkProxy: undefined } } } async execute() {} async parseVideo() { currentMedia.video = location.href; } async parseAudio() {} async parseSubtitle() {} async parseTitle() { currentMedia.title = document.title; } async parseOrigin() { currentMedia.origin = location.origin || location.href; } async parseReferer() { let index = currentUrl.indexOf('?'); currentMedia.referer = index > 0 ? currentUrl.substring(0, index) : currentUrl; } async check(video) { if (!video) { video = currentMedia.video; } if (!video || !video.startsWith('http') || video.startsWith('https://www.mp4')) { return false; } if (video.indexOf('.m3u8') > -1 || video.indexOf('.m3u') > -1) { try { const response = await (await fetch(video, { method: 'GET', credentials: 'include' })).body(); return response && response.indexOf('png') === -1; } catch (error) {} } return new RegExp(VIDEO_URL_REGEX_EXACT).test(video); } async pause() { for (let index = 0; index < MAX_TRY_COUNT; index++) { try { for (const video of document.getElementsByTagName('video')) { video.pause(); } for (const iframe of document.getElementsByTagName('iframe')) { if (iframe.contentDocument) { for (const video of iframe.contentDocument.getElementsByTagName('video')) { video.pause(); } } } } catch (error) { console.error('暂停失败', error); } finally { await sleep(REFRESH_INTERVAL * 3); } } } async close() { try { await sleep(REFRESH_INTERVAL * 2); if (window.top.history.length === 1) { window.top.location.href = "about:blank"; window.top.close(); } else { showToast(translation.onlyNewTabsCanCloseAutomatically); } } catch (error) { console.error('关闭失败', error); } } async play(player) { try { showLoading(6000); currentPlayer = player; let media = currentMedia; let parser = currentParser; let config = currentConfig.global; currentTryCount = 0; let latestError = undefined; do { currentTryCount++; try { await parser.execute(); if (await parser.check()) { latestError = undefined; break; } await sleep(REFRESH_INTERVAL * 2); } catch (error) { latestError = error; console.error(`第${currentTryCount}次尝试解析失败:`, error); } } while (currentTryCount < MAX_TRY_COUNT); if (latestError) { showToast(translation.loadFail + ': ' + latestError.message); return; } if (!await parser.check()) { showToast(translation.loadFail); return; } if (player.playEvent) { eval(policy.createScript(player.playEvent)); } if (player.presetEvent.closeAuto) { parser.close(); } if (player.presetEvent.pauseAuto) { parser.pause(); } } catch (error) { showToast(translation.loadFail + ': ' + error.message); } finally { hideLoading(); } } } const PARSER = { YTDLP: class Parser extends BaseParser { async execute() { currentMedia.ytdlp.quality = currentConfig.global.parser.ytdlp.preferredQuality === 'unlimited' ? undefined : currentConfig.global.parser.ytdlp.preferredQuality; currentMedia.ytdlp.networkProxy = currentConfig.global.networkProxy ? currentConfig.global.networkProxy : undefined; await this.parseVideo(); } async check() { return currentMedia.video ? true : false; } }, VIDEO: class Parser extends BaseParser { async execute() { await this.parseVideo(); await this.parseTitle(); await this.parseReferer(); } async parseVideo() { for (const video of document.getElementsByTagName('video')) { if (await this.check(video.src)) { currentMedia.video = video.src; return; } } for (const iframe of document.getElementsByTagName('iframe')) { if (iframe.contentDocument) { for (const video of iframe.contentDocument.getElementsByTagName('video')) { if (await this.check(video.src)) { currentMedia.video = video.src; return; } } } } } }, URL: class Parser extends BaseParser { async execute() { await this.parseVideo(); await this.parseTitle(); await this.parseReferer(); } async parseVideo() { let urls = currentUrl.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const url of urls) { if (await this.check(url)) { currentMedia.video = url; return; } } for (const iframe of document.getElementsByTagName('iframe')) { let urls = iframe.src.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const url of urls) { if (await this.check(url)) { currentMedia.video = url; return; } } } } }, HTML: class Parser extends BaseParser { async execute() { await this.parseVideo(); await this.parseTitle(); await this.parseReferer(); } async parseVideo() { let urls = document.body.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const url of urls) { if (await this.check(url)) { currentMedia.video = url; return; } } for (const iframe of document.getElementsByTagName('iframe')) { const doc = iframe.contentDocument || iframe.contentWindow.document; if (!doc) { continue; } urls = doc.body.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const url of urls) { if (await this.check(url)) { currentMedia.video = url; return; } } } } }, SCRIPT: class Parser extends BaseParser { async execute() { await this.parseVideo(); await this.parseTitle(); await this.parseReferer(); } async parseVideo() { for (const script of document.scripts) { let urls = script.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const url of urls) { if (await this.check(url)) { currentMedia.video = url; return; } } } for (const iframe of document.getElementsByTagName('iframe')) { const doc = iframe.contentDocument || iframe.contentWindow.document; if (!doc) { continue; } for (const script of doc.scripts) { let urls = script.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const url of urls) { if (await this.check(url)) { currentMedia.video = url; return; } } } } } }, REQUEST: class Parser extends BaseParser { constructor() { super(); this.video = undefined; let that = this; const open = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, async, user, password) { if (!that.video) { let urls = url.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const vurl of urls) { that.check(vurl).check().then( result => { if (result === true) { that.video = vurl; } } ) } } return open.apply(this, arguments); }; const originalFetch = fetch; window.fetch = function (url, options) { return originalFetch(url, options).then(response => { alert(url); if (!that.video) { let urls = url.match(VIDEO_URL_REGEX_GLOBAL) || []; for (const vurl of urls) { that.check(vurl).check().then( result => { if (result === true) { that.video = vurl; } } ) } } return response; }); }; } async execute() { await this.parseVideo(); } async parseVideo() { currentMedia.video = this.video; } }, BILIBILI: class Parser extends BaseParser { async execute() { await this.parseTitle(); await this.parseVideo(); await this.parseReferer(); } async parseVideo() { let videoInfo = undefined; //await this.getVideoInfo(); if (!videoInfo || !videoInfo.aid || !videoInfo.cid) { if (currentUrl.startsWith('https://www.bilibili.com/bangumi/')) { videoInfo = await this.getVideoInfoByEpid(); } else { videoInfo = await this.getVideoInfoByBvid(); } } if (!videoInfo || !videoInfo.aid || !videoInfo.cid) { throw new Error('can not find aid and cid'); } const aid = videoInfo.aid; const cid = videoInfo.cid; const title = videoInfo.title; const codecid = currentConfig.global.parser.bilibili.preferredCodec; const quality = currentConfig.global.parser.bilibili.preferredQuality; currentMedia.bilibili.cid = cid; currentMedia.title = title ? title : currentMedia.title; if (currentConfig.global.parser.bilibili.preferredSubtitle && currentConfig.global.parser.bilibili.preferredSubtitle !== 'off') { currentMedia.subtitle = await this.getSubtitle(aid, cid); } // 支持传入音频优先获取 dash 格式视频,以支持更高分辨率 if (currentPlayer.playEvent && currentPlayer.playEvent.indexOf('audio') > -1) { const dash = await this.getDash(aid, cid, codecid, quality); if (dash) { currentMedia.audio = dash.audio; currentMedia.video = dash.video; return; } } currentMedia.video = await this.getFlvOrMP4(aid, cid); } async getVideoInfo() { try { const initialState = __INITIAL_STATE__; if (!initialState) { return; } const videoInfo = initialState.epInfo || initialState.videoData || initialState.videoInfo; const aid = videoInfo.aid; const page = initialState.p; let cid = videoInfo.cid; let title = videoInfo.title; if (page && page > 1) { cid = initialState.cidMap[aid].cids[page]; } return { aid: aid, cid: cid, title: title }; } catch (error) { console.error(error.message); } } async getVideoInfoByBvid() { let param = undefined; const bvids = currentUrl.match(/BV([0-9a-zA-Z]+)/); if (bvids && bvids[1]) { param = `bvid=${bvids[1]}`; } else { const avids = page.url.match(/av([0-9]+)/); param = `aid=${avids[1]}`; } if (!param) { throw new Error('can not find bvid or avid'); } const response = await (await fetch(`https://api.bilibili.com/x/web-interface/view?${param}`, { method: 'GET', credentials: 'include' })).json(); let aid = response.data.aid; let cid = response.data.cid; let title = response.data.title; // 分 p 视频 let index = currentUrl.indexOf("?p="); if (index > -1 && response.data.pages.length > 1) { let p = currentUrl.substring(index + 3); let endIndex = p.indexOf("&"); if (endIndex > -1) { p = p.substring(0, endIndex); } const currentPage = res.data.pages[p - 1]; cid = currentPage.cid; title = currentPage.part; } return { aid: aid, cid: cid, title: title }; } async getVideoInfoByEpid() { let epid = undefined; let epids = currentUrl.match(/ep(\d+)/); if (epids && epids[1]) { epid = epids[1]; } else { let epidElement = undefined; let epidElementClassNames = [ "ep-item cursor visited", "ep-item cursor", "numberListItem_select__WgCVr", "imageListItem_wrap__o28QW", ]; for (const className of epidElementClassNames) { epidElement = document.getElementsByClassName(className)[0]; if (epidElement) { epid = epidElement.getElementsByTagName("a")[0].href.match(/ep(\d+)/)[1]; break; } } if (!epid) { epidElement = document.getElementsByClassName("squirtle-pagelist-select-item active squirtle-blink")[0]; if (epidElement) { epid = epidElement.dataset.value; } } } if (!epid) { throw new Error('can not find epid'); } const response = await (await fetch(`https://api.bilibili.com/pgc/view/web/season?ep_id=${epid}`, { method: 'GET', credentials: 'include' })).json(); let section = response.result.section; if (!section) { section = new Array(); } section.push({ episodes: response.result.episodes }); let currentEpisode; for (let i = section.length - 1; i >= 0; i--) { let episodes = section[i].episodes; for (const episode of episodes) { if (episode.id == epid) { currentEpisode = episode; break; } } if (currentEpisode) { return { aid: currentEpisode.aid, cid: currentEpisode.cid, title: currentEpisode.share_copy } } } } async getDash(aid, cid, codecid, quality) { const url = `https://api.bilibili.com/x/player/playurl?qn=120&otype=json&fourk=1&fnver=0&fnval=4048&avid=${aid}&cid=${cid}`; const response = await (await fetch(url, { method: 'GET', credentials: 'include' })).json(); if (!response.data) { currentTryCount = MAX_TRY_COUNT; throw new Error(translation.requireLoginOrVip); } let video = undefined; let audio = undefined; let dash = response.data.dash; if (!dash) { return undefined; } let hiRes = dash.flac; let dolby = dash.dolby; if (hiRes && hiRes.audio) { audio = hiRes.audio.baseUrl; } else if (dolby && dolby.audio) { audio = dolby.audio[0].base_url; } else if (dash.audio) { audio = dash.audio[0].baseUrl; } let i = 0; while (i < dash.video.length && dash.video[i].id > quality) { i++; } video = dash.video[i].baseUrl; let id = dash.video[i].id; while (i < dash.video.length) { if (dash.video[i].id != id) { break; } if (dash.video[i].codecid == codecid) { video = dash.video[i].baseUrl; break; } i++; } return { video: video, audio: audio }; } async getFlvOrMP4(aid, cid) { const url = `https://api.bilibili.com/x/player/playurl?qn=120&otype=json&fourk=1&fnver=0&fnval=128&avid=${aid}&cid=${cid}`; const response = await (await fetch(url, { method: 'GET', credentials: 'include' })).json(); if (!response.data) { currentTryCount = MAX_TRY_COUNT; throw new Error(translation.requireLoginOrVip); } return response.data.durl[0].url; } async getSubtitle(avid, cid) { const url = `https://api.bilibili.com/x/player/wbi/v2?aid=${avid}&cid=${cid}`; const response = await (await fetch(url, { method: 'GET', credentials: 'include' })).json(); if (response.code === 0 && response.data.subtitle && response.data.subtitle.subtitles.length > 0) { let subtitles = response.data.subtitle.subtitles; let url = subtitles[0].subtitle_url; let lan = subtitles[0].lan; for (const subtitle of subtitles) { if (currentConfig.global.parser.bilibili.preferredSubtitle.startsWith("zh") && subtitle.lan.startsWith("zh")) { url = subtitle.subtitle_url; lan = subtitle.lan; } if (subtitle.lan == currentConfig.subtitlePrefer) { url = subtitle.subtitle_url; lan = subtitle.lan; break; } } if (url) { return `https://www.lckp.top/common/bilibili/jsonToSrt/?url=https:${url}&lan=${lan}`; } } } }, BILIBILI_LIVE: class Parser extends BaseParser { async execute() { await this.parseVideo(); await this.parseTitle(); await this.parseReferer(); } async parseVideo() { let iframes = document.getElementsByTagName("iframe"); let roomid = undefined; for (let iframe of iframes) { let roomids = iframe.src.match( /^https:\/\/live\.bilibili\.com.*(roomid=\d+|blanc\/\d+).*/ ); if (roomids && roomids[1]) { roomid = roomids[1].match(/\d+/)[0]; break; } } if (!roomid) { throw new Error('can not find roomid'); } const quality = currentConfig.global.parser.bilibiliLive.preferredQuality; const url = `https://api.live.bilibili.com/room/v1/Room/playUrl?quality=${quality}&cid=${roomid}`; const response = await (await fetch(url, { method: 'GET', credentials: 'include' })).json(); const durls = response.data.durl; const line = currentConfig.global.parser.bilibiliLive.preferredLine; let durl = durls[durls.length - 1]; for (let index = 0; index < durls.length; index++) { if (line == index) { durl = durls[index]; break; } } currentMedia.video = durl.url; } } }; function compress(str) { return btoa(String.fromCharCode(...pako.gzip(str))); }; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function loadConfig() { let config = GM_getValue('config'); if (config) { if (config.global.version === defaultConfig.global.version) { return config; } console.log('更新配置 ......'); config = updateConfig(defaultConfig, config); config.global.version = defaultConfig.global.version; } else { console.log('初始化配置 ......'); config = JSON.parse(JSON.stringify(defaultConfig)); for (const key in config.global.parser) { config.global.parser[key].regex = []; } } GM_setValue('config', config); return config; } function updateConfig(defaultConfig, config) { function mergeDefaults(defaultObj, currentObj) { if (typeof defaultObj !== 'object' || defaultObj === null) { return currentObj !== undefined ? currentObj : defaultObj; } if (Array.isArray(defaultObj)) { return Array.isArray(currentObj) ? currentObj : defaultObj; } const merged = {}; for (const key in defaultObj) { if (key === 'regex') { merged[key] = currentObj?. [key] || []; continue; } merged[key] = mergeDefaults(defaultObj[key], currentObj?. [key]); } return merged; } const newConfig = mergeDefaults(defaultConfig, config); for (let index = 0; index < defaultConfig.players.length; index++) { const dp = defaultConfig.players[index]; const np = newConfig.players[index]; if (dp.name === np.name) { np.readonly = dp.readonly; np.playEvent = dp.playEvent; } else { newConfig.players.unshift(dp); } } return newConfig; } function matchParser(parser, url) { for (const key in parser) { for (const regex of parser[key].regex) { if (!regex || regex.startsWith('#') || regex.startsWith('//')) { continue; } if (new RegExp(regex).test(url)) { console.log(`match parser regex: ${new RegExp(regex)}`); return new PARSER[key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()](); } } } } // =================================== 按钮区域和设置页面 =================================== const policy = window.trustedTypes.createPolicy('externalPlayer', { createHTML: (string, sink) => string, createScript: (input) => input }) const ID_PREFIX = 'LCKP-EP-2024'; const FIRST_Z_INDEX = 999999999; const SECOND_Z_INDEX = FIRST_Z_INDEX - 1; const THIRD_Z_INDEX = SECOND_Z_INDEX - 1; const COLORS = [{ // 配色方案1 PRIMARY: 'rgba(245, 166, 35, 1)', TEXT: 'rgba(90, 90, 90, 1)', TEXT_ACTIVE: 'rgba(255, 255, 255, 1)', WARNING: 'rgba(233, 78, 119, 1)', BORDER: 'rgba(243, 229, 213, 1)', }, { // 配色方案2 PRIMARY: 'rgba(60, 179, 113, 1)', TEXT: 'rgba(47, 79, 79, 1)', TEXT_ACTIVE: 'rgba(255, 255, 255, 1)', WARNING: 'rgba(255, 111, 97, 1)', BORDER: 'rgba(204, 231, 208, 1)', }, { // 配色方案3 PRIMARY: 'rgba(74, 144, 226, 1)', TEXT: 'rgba(51, 51, 51, 1)', TEXT_ACTIVE: 'rgba(255, 255, 255, 1)', WARNING: 'rgba(242, 95, 92, 1)', BORDER: 'rgba(217, 227, 240, 1)', }] const COLOR = COLORS[2]; var style; var buttonDiv; var toastDiv; var loadingDiv; var settingButton; var settingIframe; var loadingId; var isReloading = false; function appendCss() { if (style) { return; } style = document.createElement('style'); style.innerHTML = policy.createHTML(` #${ID_PREFIX}-toast-div { z-index: ${FIRST_Z_INDEX}; position: fixed; top: 20px; left: 50%; transform: translate(-50%, 0); background-color: rgba(0, 0, 0, 0.8); color: white; font-size: 14px; padding: 10px 20px; border-radius: 5px; opacity: 0; transition: opacity 0.5s ease; display: none; letter-spacing: 1px; } #${ID_PREFIX}-loading-div { z-index: ${FIRST_Z_INDEX}; display: none; position: fixed; bottom: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0); } #${ID_PREFIX}-loading-div div { width: 50px; height: 50px; background-color: ${COLOR.PRIMARY}; border-radius: 0; -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out; animation: sk-rotateplane 1.2s infinite ease-in-out; } @-webkit-keyframes sk-rotateplane { 0% { -webkit-transform: perspective(120px) } 50% { -webkit-transform: perspective(120px) rotateY(180deg) } 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) } } @keyframes sk-rotateplane { 0% { transform: perspective(120px) rotateX(0deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg) } 50% { transform: perspective(120px) rotateX(-180deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(0deg) } 100% { transform: perspective(120px) rotateX(-180deg) rotateY(-180deg); -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-180deg); } } #${ID_PREFIX}-button-div { z-index: ${THIRD_Z_INDEX}; position: fixed; display: none; align-items: center; width: auto; height: auto; left: ${currentConfig.global.buttonXCoord}px; bottom: ${currentConfig.global.buttonYCoord}px; padding: 5px; border: 3px solid rgba(0, 0, 0, 0); border-radius: 5px; cursor: move; gap: 10px; background-color: rgba(0, 0, 0, 0); min-width: ${50 * currentConfig.global.buttonScale}px; min-height: ${50 * currentConfig.global.buttonScale}px; } #${ID_PREFIX}-button-div button { color: white; font-size: 20px; font-weight: bold; width: 50px; height: 50px; outline: none; border: none; border-radius: 50%; cursor: pointer; background-size: cover; background-color: rgba(0, 0, 0, 0); transition: opacity 0.5s ease, visibility 0s linear 0.5s; } #${ID_PREFIX}-button-div:hover { background-color: rgb(255, 255, 255, 0.3) !important; } #${ID_PREFIX}-button-div:hover button { visibility: visible !important; transition: opacity 0.5s ease, visibility 0s; } #${ID_PREFIX}-button-div button:hover { transform: scale(1.06); box-shadow: 0px 0px 16px #e6e6e6; } #${ID_PREFIX}-setting-button { visibility: hidden; position: absolute; right: ${-12 * currentConfig.global.buttonScale}px !important; top: ${-12 * currentConfig.global.buttonScale}px !important; width: ${25 * currentConfig.global.buttonScale}px !important; height: ${25 * currentConfig.global.buttonScale}px !important; background-image: url('data:image/svg+xml,'); } #${ID_PREFIX}-setting-iframe { z-index: ${SECOND_Z_INDEX}; position: fixed; width: 1000px; height: 500px; top: 50%; left: 50%; transform: translate(-50%, -50%); border: none; border-radius: 5px; box-shadow: 0 0 16px rgba(0, 0, 0, 0.6); background-color: #fff; display: none; } `); document.head.appendChild(style); } function appendToastDiv() { const TOAST_DIV_ID = `${ID_PREFIX}-toast-div`; if (document.getElementById(TOAST_DIV_ID)) { return; } toastDiv = document.createElement('div'); toastDiv.id = TOAST_DIV_ID; document.body.appendChild(toastDiv); } function showToast(message) { toastDiv.textContent = message; toastDiv.style.opacity = '0.9'; toastDiv.style.display = 'block'; setTimeout(() => { toastDiv.style.opacity = '0'; toastDiv.style.display = 'none'; }, 5000); } function appendLoadingDiv() { const LOADING_DIV_ID = `${ID_PREFIX}-loading-div`; if (document.getElementById(LOADING_DIV_ID)) { return; } loadingDiv = document.createElement('div'); loadingDiv.id = LOADING_DIV_ID; loadingDiv.appendChild(document.createElement('div')); document.body.appendChild(loadingDiv); } function showLoading(timeout) { if (loadingId) { clearTimeout(loadingId); loadingId = undefined; } if (!timeout) { timeout = 10000; } loadingDiv.style.display = 'block'; loadingId = setTimeout(() => { if (loadingDiv.style.display === 'block') { hideLoading(); showToast(translation.loadTimeout); } }, timeout); } function hideLoading() { loadingDiv.style.display = 'none'; } function appendButtonDiv() { const BUTTON_DIV_ID = `${ID_PREFIX}-button-div`; if (document.getElementById(BUTTON_DIV_ID)) { return; } buttonDiv = document.createElement('div'); buttonDiv.id = BUTTON_DIV_ID; buttonDiv.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') { return; } let offsetX = e.clientX - buttonDiv.getBoundingClientRect().left; let offsetY = e.clientY - buttonDiv.getBoundingClientRect().top; document.addEventListener('mouseup', mouseUpHandler); document.addEventListener('mousemove', mouseMoveHandler); function mouseUpHandler() { buttonDiv.style.border = '3px solid rgba(0, 0, 0, 0)'; document.removeEventListener('mousemove', mouseMoveHandler); document.removeEventListener('mouseup', mouseUpHandler); } function mouseMoveHandler(e) { buttonDiv.style.border = `3px solid ${COLOR.PRIMARY}`; let newX = e.clientX - offsetX; let newY = e.clientY - offsetY; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const divWidth = buttonDiv.offsetWidth; const divHeight = buttonDiv.offsetHeight; if (newX < 0) newX = 0; if (newX + divWidth > windowWidth) newX = windowWidth - divWidth; if (newY < 0) newY = 0; if (newY + divHeight > windowHeight) newY = windowHeight - divHeight; newY = windowHeight - newY - divHeight; buttonDiv.style.left = `${newX}px`; buttonDiv.style.bottom = `${newY}px`; currentConfig.global.buttonXCoord = newX; currentConfig.global.buttonYCoord = newY; GM_setValue('config', currentConfig); } }); document.body.appendChild(buttonDiv); appendPlayButton(); appendSettingButton(); // 全屏隐藏 document.addEventListener("fullscreenchange", () => { if (document.fullscreenElement) { buttonDiv.style.display = "none"; } else { if (currentParser) { buttonDiv.style.display = "flex"; } } }); } function appendPlayButton() { if (!currentConfig.players) { return; } var playButtonNeedAutoClick; currentConfig.players.forEach(player => { if (player.enable !== true) { return; } const playButton = document.createElement('button'); if (player.icon) { const image = new Image(); image.src = player.icon; image.onload = () => playButton.style.backgroundImage = `url(${image.src})`; image.onerror = () => { playButton.style.backgroundColor = COLOR.PRIMARY; playButton.textContent = player.name ? player.name.substring(0, 1) : 'P'; }; } else { playButton.style.backgroundColor = COLOR.PRIMARY; playButton.textContent = player.name ? player.name.substring(0, 1) : 'P'; } playButton.style.width = `${player.iconSize * currentConfig.global.buttonScale}px`; playButton.style.height = `${player.iconSize * currentConfig.global.buttonScale}px`; // 自动隐藏 if (currentConfig.global.buttonVisibilityDuration == 0) { playButton.style.visibility = 'hidden'; } else if (currentConfig.global.buttonVisibilityDuration > 0) { setTimeout(() => { playButton.style.visibility = 'hidden'; }, currentConfig.global.buttonVisibilityDuration); } playButton.addEventListener('click', async function () { if (currentParser) { currentParser.play(player); } else { showToast(translation.noMatchingParserFound); } }); buttonDiv.appendChild(playButton); }); } function appendSettingButton() { settingButton = document.createElement('button'); settingButton.id = `${ID_PREFIX}-setting-button`; settingButton.title = 'Ctrl + Alt + E'; settingButton.addEventListener('click', async () => { await appendSettingIframe(); if (settingIframe.style.display === "block") { settingIframe.style.display = "none"; } else { settingIframe.contentWindow.postMessage({ defaultConfig: defaultConfig, config: currentConfig }, '*'); settingIframe.style.display = "block"; } }); buttonDiv.appendChild(settingButton); // 失去焦点隐藏设置页面 document.addEventListener('click', (event) => { if (settingIframe && settingIframe.style.display === 'block' && !settingButton.contains(event.target) && !settingIframe.contains(event.target)) { settingIframe.style.display = 'none'; } }); } async function appendSettingIframe() { const SETTING_IFRAME_ID = `${ID_PREFIX}-setting-iframe`; if (document.getElementById(SETTING_IFRAME_ID)) { return; } settingIframe = document.createElement('iframe'); settingIframe.id = SETTING_IFRAME_ID; let settingIframeHtml = ` External Player
无限制
2160P
1440P
1080P
720P
无限制
2160P
1080P
720P
关闭
简体
繁体
English
HEVC
AV1
AVC
原画
高清
流畅
主线
备线1
备线2
备线3
`; if (SETTING_URL) { const response = await fetch(SETTING_URL); settingIframeHtml = await response.text(); } settingIframe.onload = function () { const doc = settingIframe.contentDocument || settingIframe.contentWindow.document; doc.open(); doc.write(policy.createHTML(settingIframeHtml)); doc.close(); }; document.body.appendChild(settingIframe); window.addEventListener('message', function (event) { if (event.data && event.data.global) { // 保存配置 currentConfig = event.data; GM_setValue('config', currentConfig); showToast(translation.saveSuccessfully); // 移除旧元素 document.head.removeChild(style); document.body.removeChild(buttonDiv); style = undefined; buttonDiv = undefined; // 重新初始化 isReloading = true; init(currentUrl); } }); try { showLoading(); await sleep(REFRESH_INTERVAL); } finally { hideLoading(); } } function startFlashing(element) { let visibility = element.style.visibility; let transition = element.style.transition; let boxShadow = element.style.boxShadow; element.style.visibility = 'visible'; element.style.transition = 'box-shadow 0.5s ease'; let isGlowing = false; const interval = setInterval(() => { isGlowing = !isGlowing; element.style.boxShadow = isGlowing ? `0 0 10px 10px ${COLOR.PRIMARY}` : 'none'; }, 500); setTimeout(() => { clearInterval(interval); element.style.visibility = visibility; element.transition = transition; element.boxShadow = boxShadow; }, 5000); } // ======================================== 开始执行 ======================================= function init(url) { currentConfig = loadConfig(); translation = translations[currentConfig.global.language]; appendCss(); appendToastDiv(); appendLoadingDiv(); appendButtonDiv(); currentParser = matchParser(currentConfig.global.parser, url) || matchParser(defaultConfig.global.parser, url); if (currentParser) { buttonDiv.style.display = 'flex'; if (!isReloading) { for (const player of currentConfig.players) { if (player.presetEvent.playAuto === true) { currentParser.play(player); } } } isReloading = false; } currentUrl = url; } onload = () => { setInterval(() => { const url = location.href; if (currentUrl !== url || !buttonDiv) { console.log(`current url update: ${currentUrl ? currentUrl + ' => ' : ''}${url}`); init(url); } }, REFRESH_INTERVAL); // 快捷键 document.addEventListener('keydown', (event) => { // 打开设置:Ctrl + Alt + E if (event.ctrlKey && event.altKey && (event.key === 'e' || event.key === 'E')) { event.preventDefault(); startFlashing(settingButton); settingButton.click(); } }); };