External Player

Play web video via external player

目前為 2024-11-24 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name External Player
  3. // @name:zh-CN 外部播放器
  4. // @namespace https://github.com/LuckyPuppy514/external-player
  5. // @copyright 2024, Grant LuckyPuppy514 (https://github.com/LuckyPuppy514)
  6. // @version 1.0.0
  7. // @license MIT
  8. // @description Play web video via external player
  9. // @description:zh-CN 使用外部播放器播放网页中的视频
  10. // @icon https://www.lckp.top/gh/LuckyPuppy514/pic-bed/common/mpv.png
  11. // @author LuckyPuppy514
  12. // @homepage https://github.com/LuckyPuppy514/external-player
  13. // @include *://*
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @run-at document-end
  17. // @require https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/pako/2.0.4/pako.min.js
  18. // ==/UserScript==
  19.  
  20. 'use strict';
  21.  
  22. const SETTING_URL = undefined;
  23. // const SETTING_URL = 'http://127.0.0.1:5500/setting.html';
  24.  
  25. 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;
  26.  
  27. 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;
  28.  
  29. const defaultConfig = {
  30. global: {
  31. version: '1.0.0',
  32. language: (navigator.language || navigator.userLanguage) === 'zh-CN' ? 'zh' : 'en',
  33. buttonXCoord: '0',
  34. buttonYCoord: '0',
  35. buttonScale: '1.00',
  36. buttonVisibilityDuration: '5000',
  37. networkProxy: '',
  38. parser: {
  39. ytdlp: {
  40. regex: [
  41. "https://www.youtube.com/watch\\?.+",
  42. "https://www.youtube.com/playlist\\?list=.+",
  43. "https://www.lckp.top/play-with-mpv/index.html",
  44. "https://www.piccsz.cn/git/",
  45. ],
  46. preferredQuality: 'unlimited',
  47. },
  48. video: {
  49. regex: [
  50. "https://www.libvio.fun/play/.+",
  51. "https://www.tucao.my/play/.+",
  52. "https://ddys.pro/.+"
  53. ]
  54. },
  55. url: {
  56. regex: [
  57. "https://anime.girigirilove.com/play.+",
  58. ]
  59. },
  60. html: {
  61. regex: []
  62. },
  63. script: {
  64. regex: []
  65. },
  66. request: {
  67. regex: []
  68. },
  69. bilibili: {
  70. regex: [
  71. "https://www.bilibili.com/bangumi/play/.+",
  72. "https://www.bilibili.com/video/.+",
  73. "https://www.bilibili.com/list/.+",
  74. "https://www.bilibili.com/festival/.+"
  75. ],
  76. preferredQuality: '127',
  77. preferredSubtitle: 'off',
  78. preferredCodec: '12',
  79. },
  80. bilibiliLive: {
  81. regex: [
  82. "https://live.bilibili.com/\\d+.*",
  83. "https://live.bilibili.com/blanc/\\d+.*",
  84. "https://live.bilibili.com/blackboard/era/.+",
  85. ],
  86. preferredQuality: '4',
  87. preferredLine: '0',
  88. }
  89. }
  90. },
  91. players: [{
  92. name: 'IINA',
  93. system: 'mac',
  94. icon: 'https://upload.wikimedia.org/wikipedia/commons/5/51/IINA_Logo.png',
  95. iconSize: 60,
  96. 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');",
  97. presetEvent: {
  98. playAuto: false,
  99. pauseAuto: true,
  100. closeAuto: false
  101. },
  102. enable: true,
  103. readonly: true,
  104. }, {
  105. name: 'PotPlayer',
  106. system: 'windows',
  107. icon: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/PotPlayer_logo_%282017%29.png',
  108. iconSize: 50,
  109. 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');",
  110. presetEvent: {
  111. playAuto: false,
  112. pauseAuto: true,
  113. closeAuto: false
  114. },
  115. enable: true,
  116. readonly: true,
  117. },
  118. {
  119. name: 'MPV',
  120. system: 'windows',
  121. icon: 'https://upload.wikimedia.org/wikipedia/commons/6/66/Unofficial_Mpv_logo_%28with_gradients%29.svg',
  122. iconSize: 55,
  123. 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');",
  124. presetEvent: {
  125. playAuto: false,
  126. pauseAuto: true,
  127. closeAuto: false
  128. },
  129. enable: true,
  130. readonly: true,
  131. }
  132. ]
  133. }
  134.  
  135. const translations = {
  136. en: {
  137. loadSuccessfully: 'Load successfully',
  138. loadTimeout: 'Load timeout ......',
  139. saveSuccessfully: 'Save successfully',
  140. loadFail: 'Load fail',
  141. requireLoginOrVip: 'Require login or vip',
  142. noMatchingParserFound: 'No matching parser found',
  143. onlyNewTabsCanCloseAutomatically: 'Only new tabs can close automatically'
  144. },
  145. zh: {
  146. loadSuccessfully: '加载成功',
  147. loadTimeout: '加载超时 ......',
  148. saveSuccessfully: '保存成功',
  149. loadFail: '加载失败',
  150. requireLoginOrVip: '需要登录或会员',
  151. noMatchingParserFound: '没有匹配的解析器',
  152. onlyNewTabsCanCloseAutomatically: '只有新标签页才能自动关闭'
  153. }
  154. };
  155.  
  156. const REFRESH_INTERVAL = 500;
  157. const MAX_TRY_COUNT = 5;
  158.  
  159. var currentTryCount;
  160. var currentConfig;
  161. var currentUrl;
  162. var currentParser;
  163. var currentMedia;
  164. var currentPlayer;
  165. var translation;
  166.  
  167. class BaseParser {
  168. constructor() {
  169. currentMedia = {
  170. video: undefined,
  171. audio: undefined,
  172. subtitle: undefined,
  173. title: undefined,
  174. origin: undefined,
  175. referer: undefined,
  176. bilibili: {
  177. cid: undefined
  178. },
  179. ytdlp: {
  180. quality: undefined,
  181. networkProxy: undefined
  182. }
  183. }
  184. }
  185. async execute() {}
  186. async parseVideo() {
  187. currentMedia.video = location.href;
  188. }
  189. async parseAudio() {}
  190. async parseSubtitle() {}
  191. async parseTitle() {
  192. currentMedia.title = document.title;
  193. }
  194. async parseOrigin() {
  195. currentMedia.origin = location.origin || location.href;
  196. }
  197. async parseReferer() {
  198. let index = currentUrl.indexOf('?');
  199. currentMedia.referer = index > 0 ? currentUrl.substring(0, index) : currentUrl;
  200. }
  201. async check(video) {
  202. if (!video) {
  203. video = currentMedia.video;
  204. }
  205. if (!video || !video.startsWith('http') || video.startsWith('https://www.mp4')) {
  206. return false;
  207. }
  208.  
  209. if (video.indexOf('.m3u8') > -1 || video.indexOf('.m3u') > -1) {
  210. try {
  211. const response = await (await fetch(video, {
  212. method: 'GET',
  213. credentials: 'include'
  214. })).body();
  215. return response && response.indexOf('png') === -1;
  216. } catch (error) {}
  217. }
  218. return new RegExp(VIDEO_URL_REGEX_EXACT).test(video);
  219. }
  220. async pause() {
  221. for (let index = 0; index < MAX_TRY_COUNT; index++) {
  222. try {
  223. for (const video of document.getElementsByTagName('video')) {
  224. video.pause();
  225. }
  226.  
  227. for (const iframe of document.getElementsByTagName('iframe')) {
  228. if (iframe.contentDocument) {
  229. for (const video of iframe.contentDocument.getElementsByTagName('video')) {
  230. video.pause();
  231. }
  232. }
  233. }
  234. } catch (error) {
  235. console.error('暂停失败', error);
  236. } finally {
  237. await sleep(REFRESH_INTERVAL * 3);
  238. }
  239. }
  240. }
  241. async close() {
  242. try {
  243. await sleep(REFRESH_INTERVAL * 2);
  244. if (window.top.history.length === 1) {
  245. window.top.location.href = "about:blank";
  246. window.top.close();
  247. } else {
  248. showToast(translation.onlyNewTabsCanCloseAutomatically);
  249. }
  250. } catch (error) {
  251. console.error('关闭失败', error);
  252. }
  253. }
  254. async play(player) {
  255. try {
  256. showLoading(6000);
  257. currentPlayer = player;
  258. let media = currentMedia;
  259. let parser = currentParser;
  260. let config = currentConfig.global;
  261.  
  262. currentTryCount = 0;
  263. let latestError = undefined;
  264. do {
  265. currentTryCount++;
  266. try {
  267. await parser.execute();
  268. if (await parser.check()) {
  269. latestError = undefined;
  270. break;
  271. }
  272. await sleep(REFRESH_INTERVAL * 2);
  273. } catch (error) {
  274. latestError = error;
  275. console.error(`第${currentTryCount}次尝试解析失败:`, error);
  276. }
  277. }
  278. while (currentTryCount < MAX_TRY_COUNT);
  279. if (latestError) {
  280. showToast(translation.loadFail + ': ' + latestError.message);
  281. return;
  282. }
  283. if (!await parser.check()) {
  284. showToast(translation.loadFail);
  285. return;
  286. }
  287.  
  288. if (player.playEvent) {
  289. eval(policy.createScript(player.playEvent));
  290. }
  291.  
  292. if (player.presetEvent.closeAuto) {
  293. parser.close();
  294. }
  295. if (player.presetEvent.pauseAuto) {
  296. parser.pause();
  297. }
  298. } catch (error) {
  299. showToast(translation.loadFail + ': ' + error.message);
  300. } finally {
  301. hideLoading();
  302. }
  303. }
  304. }
  305.  
  306. const PARSER = {
  307. YTDLP: class Parser extends BaseParser {
  308. async execute() {
  309. currentMedia.ytdlp.quality = currentConfig.global.parser.ytdlp.preferredQuality === 'unlimited' ?
  310. undefined :
  311. currentConfig.global.parser.ytdlp.preferredQuality;
  312. currentMedia.ytdlp.networkProxy = currentConfig.global.networkProxy ?
  313. currentConfig.global.networkProxy :
  314. undefined;
  315. await this.parseVideo();
  316. }
  317. async check() {
  318. return currentMedia.video ? true : false;
  319. }
  320. },
  321. VIDEO: class Parser extends BaseParser {
  322. async execute() {
  323. await this.parseVideo();
  324. await this.parseTitle();
  325. await this.parseReferer();
  326. }
  327. async parseVideo() {
  328. for (const video of document.getElementsByTagName('video')) {
  329. if (await this.check(video.src)) {
  330. currentMedia.video = video.src;
  331. return;
  332. }
  333. }
  334.  
  335. for (const iframe of document.getElementsByTagName('iframe')) {
  336. if (iframe.contentDocument) {
  337. for (const video of iframe.contentDocument.getElementsByTagName('video')) {
  338. if (await this.check(video.src)) {
  339. currentMedia.video = video.src;
  340. return;
  341. }
  342. }
  343. }
  344. }
  345. }
  346. },
  347. URL: class Parser extends BaseParser {
  348. async execute() {
  349. await this.parseVideo();
  350. await this.parseTitle();
  351. await this.parseReferer();
  352. }
  353. async parseVideo() {
  354. let urls = currentUrl.match(VIDEO_URL_REGEX_GLOBAL) || [];
  355. for (const url of urls) {
  356. if (await this.check(url)) {
  357. currentMedia.video = url;
  358. return;
  359. }
  360. }
  361.  
  362. for (const iframe of document.getElementsByTagName('iframe')) {
  363. let urls = iframe.src.match(VIDEO_URL_REGEX_GLOBAL) || [];
  364. for (const url of urls) {
  365. if (await this.check(url)) {
  366. currentMedia.video = url;
  367. return;
  368. }
  369. }
  370. }
  371. }
  372. },
  373. HTML: class Parser extends BaseParser {
  374. async execute() {
  375. await this.parseVideo();
  376. await this.parseTitle();
  377. await this.parseReferer();
  378. }
  379. async parseVideo() {
  380. let urls = document.body.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || [];
  381. for (const url of urls) {
  382. if (await this.check(url)) {
  383. currentMedia.video = url;
  384. return;
  385. }
  386. }
  387.  
  388. for (const iframe of document.getElementsByTagName('iframe')) {
  389. const doc = iframe.contentDocument || iframe.contentWindow.document;
  390. if (!doc) {
  391. continue;
  392. }
  393. urls = doc.body.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || [];
  394. for (const url of urls) {
  395. if (await this.check(url)) {
  396. currentMedia.video = url;
  397. return;
  398. }
  399. }
  400. }
  401. }
  402. },
  403. SCRIPT: class Parser extends BaseParser {
  404. async execute() {
  405. await this.parseVideo();
  406. await this.parseTitle();
  407. await this.parseReferer();
  408. }
  409. async parseVideo() {
  410. for (const script of document.scripts) {
  411. let urls = script.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || [];
  412. for (const url of urls) {
  413. if (await this.check(url)) {
  414. currentMedia.video = url;
  415. return;
  416. }
  417. }
  418. }
  419.  
  420. for (const iframe of document.getElementsByTagName('iframe')) {
  421. const doc = iframe.contentDocument || iframe.contentWindow.document;
  422. if (!doc) {
  423. continue;
  424. }
  425. for (const script of doc.scripts) {
  426. let urls = script.innerHTML.match(VIDEO_URL_REGEX_GLOBAL) || [];
  427. for (const url of urls) {
  428. if (await this.check(url)) {
  429. currentMedia.video = url;
  430. return;
  431. }
  432. }
  433. }
  434. }
  435. }
  436. },
  437. REQUEST: class Parser extends BaseParser {
  438. constructor() {
  439. super();
  440. this.video = undefined;
  441. let that = this;
  442. const open = XMLHttpRequest.prototype.open;
  443. XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
  444. if (!that.video) {
  445. let urls = url.match(VIDEO_URL_REGEX_GLOBAL) || [];
  446. for (const vurl of urls) {
  447. that.check(vurl).check().then(
  448. result => {
  449. if (result === true) {
  450. that.video = vurl;
  451. }
  452. }
  453. )
  454. }
  455.  
  456. }
  457. return open.apply(this, arguments);
  458. };
  459.  
  460. const originalFetch = fetch;
  461.  
  462. window.fetch = function (url, options) {
  463. return originalFetch(url, options).then(response => {
  464. alert(url);
  465. if (!that.video) {
  466. let urls = url.match(VIDEO_URL_REGEX_GLOBAL) || [];
  467. for (const vurl of urls) {
  468. that.check(vurl).check().then(
  469. result => {
  470. if (result === true) {
  471. that.video = vurl;
  472. }
  473. }
  474. )
  475. }
  476.  
  477. }
  478.  
  479. return response;
  480. });
  481. };
  482. }
  483. async execute() {
  484. await this.parseVideo();
  485. }
  486. async parseVideo() {
  487. currentMedia.video = this.video;
  488. }
  489. },
  490. BILIBILI: class Parser extends BaseParser {
  491. async execute() {
  492. await this.parseTitle();
  493. await this.parseVideo();
  494. await this.parseReferer();
  495. }
  496. async parseVideo() {
  497. let videoInfo = undefined; //await this.getVideoInfo();
  498.  
  499. if (!videoInfo || !videoInfo.aid || !videoInfo.cid) {
  500. if (currentUrl.startsWith('https://www.bilibili.com/bangumi/')) {
  501. videoInfo = await this.getVideoInfoByEpid();
  502. } else {
  503. videoInfo = await this.getVideoInfoByBvid();
  504. }
  505. }
  506.  
  507. if (!videoInfo || !videoInfo.aid || !videoInfo.cid) {
  508. throw new Error('can not find aid and cid');
  509. }
  510.  
  511. const aid = videoInfo.aid;
  512. const cid = videoInfo.cid;
  513. const title = videoInfo.title;
  514. const codecid = currentConfig.global.parser.bilibili.preferredCodec;
  515. const quality = currentConfig.global.parser.bilibili.preferredQuality;
  516.  
  517. currentMedia.bilibili.cid = cid;
  518. currentMedia.title = title ? title : currentMedia.title;
  519. if (currentConfig.global.parser.bilibili.preferredSubtitle &&
  520. currentConfig.global.parser.bilibili.preferredSubtitle !== 'off') {
  521. currentMedia.subtitle = await this.getSubtitle(aid, cid);
  522. }
  523. // 支持传入音频优先获取 dash 格式视频,以支持更高分辨率
  524. if (currentPlayer.playEvent && currentPlayer.playEvent.indexOf('audio') > -1) {
  525. const dash = await this.getDash(aid, cid, codecid, quality);
  526. if (dash) {
  527. currentMedia.audio = dash.audio;
  528. currentMedia.video = dash.video;
  529. return;
  530. }
  531. }
  532. currentMedia.video = await this.getFlvOrMP4(aid, cid);
  533. }
  534. async getVideoInfo() {
  535. try {
  536. const initialState = __INITIAL_STATE__;
  537. if (!initialState) {
  538. return;
  539. }
  540. const videoInfo = initialState.epInfo || initialState.videoData || initialState.videoInfo;
  541. const aid = videoInfo.aid;
  542. const page = initialState.p;
  543. let cid = videoInfo.cid;
  544. let title = videoInfo.title;
  545. if (page && page > 1) {
  546. cid = initialState.cidMap[aid].cids[page];
  547. }
  548.  
  549. return {
  550. aid: aid,
  551. cid: cid,
  552. title: title
  553. };
  554. } catch (error) {
  555. console.error(error.message);
  556. }
  557. }
  558. async getVideoInfoByBvid() {
  559. let param = undefined;
  560. const bvids = currentUrl.match(/BV([0-9a-zA-Z]+)/);
  561. if (bvids && bvids[1]) {
  562. param = `bvid=${bvids[1]}`;
  563. } else {
  564. const avids = page.url.match(/av([0-9]+)/);
  565. param = `aid=${avids[1]}`;
  566. }
  567.  
  568. if (!param) {
  569. throw new Error('can not find bvid or avid');
  570. }
  571.  
  572. const response = await (await fetch(`https://api.bilibili.com/x/web-interface/view?${param}`, {
  573. method: 'GET',
  574. credentials: 'include'
  575. })).json();
  576.  
  577. let aid = response.data.aid;
  578. let cid = response.data.cid;
  579. let title = response.data.title;
  580.  
  581. // 分 p 视频
  582. let index = currentUrl.indexOf("?p=");
  583. if (index > -1 && response.data.pages.length > 1) {
  584. let p = currentUrl.substring(index + 3);
  585. let endIndex = p.indexOf("&");
  586. if (endIndex > -1) {
  587. p = p.substring(0, endIndex);
  588. }
  589. const currentPage = res.data.pages[p - 1];
  590. cid = currentPage.cid;
  591. title = currentPage.part;
  592. }
  593.  
  594. return {
  595. aid: aid,
  596. cid: cid,
  597. title: title
  598. };
  599. }
  600. async getVideoInfoByEpid() {
  601. let epid = undefined;
  602. let epids = currentUrl.match(/ep(\d+)/);
  603. if (epids && epids[1]) {
  604. epid = epids[1];
  605. } else {
  606. let epidElement = undefined;
  607. let epidElementClassNames = [
  608. "ep-item cursor visited",
  609. "ep-item cursor",
  610. "numberListItem_select__WgCVr",
  611. "imageListItem_wrap__o28QW",
  612. ];
  613. for (const className of epidElementClassNames) {
  614. epidElement = document.getElementsByClassName(className)[0];
  615. if (epidElement) {
  616. epid = epidElement.getElementsByTagName("a")[0].href.match(/ep(\d+)/)[1];
  617. break;
  618. }
  619. }
  620.  
  621. if (!epid) {
  622. epidElement = document.getElementsByClassName("squirtle-pagelist-select-item active squirtle-blink")[0];
  623. if (epidElement) {
  624. epid = epidElement.dataset.value;
  625. }
  626. }
  627. }
  628.  
  629. if (!epid) {
  630. throw new Error('can not find epid');
  631. }
  632.  
  633. const response = await (await fetch(`https://api.bilibili.com/pgc/view/web/season?ep_id=${epid}`, {
  634. method: 'GET',
  635. credentials: 'include'
  636. })).json();
  637. let section = response.result.section;
  638. if (!section) {
  639. section = new Array();
  640. }
  641. section.push({
  642. episodes: response.result.episodes
  643. });
  644. let currentEpisode;
  645. for (let i = section.length - 1; i >= 0; i--) {
  646. let episodes = section[i].episodes;
  647. for (const episode of episodes) {
  648. if (episode.id == epid) {
  649. currentEpisode = episode;
  650. break;
  651. }
  652. }
  653. if (currentEpisode) {
  654. return {
  655. aid: currentEpisode.aid,
  656. cid: currentEpisode.cid,
  657. title: currentEpisode.share_copy
  658. }
  659. }
  660. }
  661. }
  662. async getDash(aid, cid, codecid, quality) {
  663. const url = `https://api.bilibili.com/x/player/playurl?qn=120&otype=json&fourk=1&fnver=0&fnval=4048&avid=${aid}&cid=${cid}`;
  664. const response = await (await fetch(url, {
  665. method: 'GET',
  666. credentials: 'include'
  667. })).json();
  668. if (!response.data) {
  669. currentTryCount = MAX_TRY_COUNT;
  670. throw new Error(translation.requireLoginOrVip);
  671. }
  672. let video = undefined;
  673. let audio = undefined;
  674. let dash = response.data.dash;
  675. if (!dash) {
  676. return undefined;
  677. }
  678. let hiRes = dash.flac;
  679. let dolby = dash.dolby;
  680. if (hiRes && hiRes.audio) {
  681. audio = hiRes.audio.baseUrl;
  682. } else if (dolby && dolby.audio) {
  683. audio = dolby.audio[0].base_url;
  684. } else if (dash.audio) {
  685. audio = dash.audio[0].baseUrl;
  686. }
  687. let i = 0;
  688. while (i < dash.video.length &&
  689. dash.video[i].id > quality) {
  690. i++;
  691. }
  692. video = dash.video[i].baseUrl;
  693. let id = dash.video[i].id;
  694. while (i < dash.video.length) {
  695. if (dash.video[i].id != id) {
  696. break;
  697. }
  698. if (dash.video[i].codecid == codecid) {
  699. video = dash.video[i].baseUrl;
  700. break;
  701. }
  702. i++;
  703. }
  704. return {
  705. video: video,
  706. audio: audio
  707. };
  708. }
  709. async getFlvOrMP4(aid, cid) {
  710. const url = `https://api.bilibili.com/x/player/playurl?qn=120&otype=json&fourk=1&fnver=0&fnval=128&avid=${aid}&cid=${cid}`;
  711. const response = await (await fetch(url, {
  712. method: 'GET',
  713. credentials: 'include'
  714. })).json();
  715. if (!response.data) {
  716. currentTryCount = MAX_TRY_COUNT;
  717. throw new Error(translation.requireLoginOrVip);
  718. }
  719. return response.data.durl[0].url;
  720. }
  721. async getSubtitle(avid, cid) {
  722. const url = `https://api.bilibili.com/x/player/wbi/v2?aid=${avid}&cid=${cid}`;
  723. const response = await (await fetch(url, {
  724. method: 'GET',
  725. credentials: 'include'
  726. })).json();
  727.  
  728. if (response.code === 0 && response.data.subtitle && response.data.subtitle.subtitles.length > 0) {
  729. let subtitles = response.data.subtitle.subtitles;
  730. let url = subtitles[0].subtitle_url;
  731. let lan = subtitles[0].lan;
  732. for (const subtitle of subtitles) {
  733. if (currentConfig.global.parser.bilibili.preferredSubtitle.startsWith("zh") &&
  734. subtitle.lan.startsWith("zh")) {
  735. url = subtitle.subtitle_url;
  736. lan = subtitle.lan;
  737. }
  738. if (subtitle.lan == currentConfig.subtitlePrefer) {
  739. url = subtitle.subtitle_url;
  740. lan = subtitle.lan;
  741. break;
  742. }
  743. }
  744. if (url) {
  745. return `https://www.lckp.top/common/bilibili/jsonToSrt/?url=https:${url}&lan=${lan}`;
  746. }
  747. }
  748. }
  749. },
  750. BILIBILI_LIVE: class Parser extends BaseParser {
  751. async execute() {
  752. await this.parseVideo();
  753. await this.parseTitle();
  754. await this.parseReferer();
  755. }
  756. async parseVideo() {
  757. let iframes = document.getElementsByTagName("iframe");
  758. let roomid = undefined;
  759. for (let iframe of iframes) {
  760. let roomids = iframe.src.match(
  761. /^https:\/\/live\.bilibili\.com.*(roomid=\d+|blanc\/\d+).*/
  762. );
  763. if (roomids && roomids[1]) {
  764. roomid = roomids[1].match(/\d+/)[0];
  765. break;
  766. }
  767. }
  768.  
  769. if (!roomid) {
  770. throw new Error('can not find roomid');
  771. }
  772.  
  773. const quality = currentConfig.global.parser.bilibiliLive.preferredQuality;
  774. const url = `https://api.live.bilibili.com/room/v1/Room/playUrl?quality=${quality}&cid=${roomid}`;
  775. const response = await (await fetch(url, {
  776. method: 'GET',
  777. credentials: 'include'
  778. })).json();
  779.  
  780. const durls = response.data.durl;
  781. const line = currentConfig.global.parser.bilibiliLive.preferredLine;
  782. let durl = durls[durls.length - 1];
  783. for (let index = 0; index < durls.length; index++) {
  784. if (line == index) {
  785. durl = durls[index];
  786. break;
  787. }
  788. }
  789. currentMedia.video = durl.url;
  790. }
  791. }
  792. };
  793.  
  794. function compress(str) {
  795. return btoa(String.fromCharCode(...pako.gzip(str)));
  796. };
  797.  
  798. function sleep(ms) {
  799. return new Promise((resolve) => setTimeout(resolve, ms));
  800. }
  801.  
  802. function loadConfig() {
  803. let config = GM_getValue('config');
  804. if (config) {
  805. if (config.global.version === defaultConfig.global.version) {
  806. return config;
  807. }
  808. console.log('更新配置 ......');
  809. config = updateConfig(defaultConfig, config);
  810. config.global.version = defaultConfig.global.version;
  811. } else {
  812. console.log('初始化配置 ......');
  813. config = JSON.parse(JSON.stringify(defaultConfig));
  814. for (const key in config.global.parser) {
  815. config.global.parser[key].regex = [];
  816. }
  817. }
  818. GM_setValue('config', config);
  819. return config;
  820. }
  821.  
  822. function updateConfig(defaultConfig, config) {
  823. function mergeDefaults(defaultObj, currentObj) {
  824. if (typeof defaultObj !== 'object' || defaultObj === null) {
  825. return currentObj !== undefined ? currentObj : defaultObj;
  826. }
  827.  
  828. if (Array.isArray(defaultObj)) {
  829. return Array.isArray(currentObj) ? currentObj : defaultObj;
  830. }
  831.  
  832. const merged = {};
  833. for (const key in defaultObj) {
  834. if (key === 'regex') {
  835. merged[key] = currentObj?. [key] || [];
  836. continue;
  837. }
  838. merged[key] = mergeDefaults(defaultObj[key], currentObj?. [key]);
  839. }
  840. return merged;
  841. }
  842.  
  843. const newConfig = mergeDefaults(defaultConfig, config);
  844. for (let index = 0; index < defaultConfig.players.length; index++) {
  845. const dp = defaultConfig.players[index];
  846. const np = newConfig.players[index];
  847. if (dp.name === np.name) {
  848. np.readonly = dp.readonly;
  849. np.playEvent = dp.playEvent;
  850. } else {
  851. newConfig.players.unshift(dp);
  852. }
  853. }
  854.  
  855. return newConfig;
  856. }
  857.  
  858. function matchParser(parser, url) {
  859. for (const key in parser) {
  860. for (const regex of parser[key].regex) {
  861. if (!regex || regex.startsWith('#') || regex.startsWith('//')) {
  862. continue;
  863. }
  864. if (new RegExp(regex).test(url)) {
  865. console.log(`match parser regex: ${new RegExp(regex)}`);
  866. return new PARSER[key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]();
  867. }
  868. }
  869. }
  870. }
  871.  
  872. // =================================== 按钮区域和设置页面 ===================================
  873.  
  874. const policy = window.trustedTypes.createPolicy('externalPlayer', {
  875. createHTML: (string, sink) => string,
  876. createScript: (input) => input
  877. })
  878.  
  879. const ID_PREFIX = 'LCKP-EP-2024';
  880. const FIRST_Z_INDEX = 999999999;
  881. const SECOND_Z_INDEX = FIRST_Z_INDEX - 1;
  882. const THIRD_Z_INDEX = SECOND_Z_INDEX - 1;
  883.  
  884. const COLORS = [{
  885. // 配色方案1
  886. PRIMARY: 'rgba(245, 166, 35, 1)',
  887. TEXT: 'rgba(90, 90, 90, 1)',
  888. TEXT_ACTIVE: 'rgba(255, 255, 255, 1)',
  889. WARNING: 'rgba(233, 78, 119, 1)',
  890. BORDER: 'rgba(243, 229, 213, 1)',
  891. }, {
  892. // 配色方案2
  893. PRIMARY: 'rgba(60, 179, 113, 1)',
  894. TEXT: 'rgba(47, 79, 79, 1)',
  895. TEXT_ACTIVE: 'rgba(255, 255, 255, 1)',
  896. WARNING: 'rgba(255, 111, 97, 1)',
  897. BORDER: 'rgba(204, 231, 208, 1)',
  898. }, {
  899. // 配色方案3
  900. PRIMARY: 'rgba(74, 144, 226, 1)',
  901. TEXT: 'rgba(51, 51, 51, 1)',
  902. TEXT_ACTIVE: 'rgba(255, 255, 255, 1)',
  903. WARNING: 'rgba(242, 95, 92, 1)',
  904. BORDER: 'rgba(217, 227, 240, 1)',
  905. }]
  906. const COLOR = COLORS[2];
  907.  
  908. var style;
  909. var buttonDiv;
  910. var toastDiv;
  911. var loadingDiv;
  912. var settingButton;
  913. var settingIframe;
  914. var loadingId;
  915. var isReloading = false;
  916.  
  917. function appendCss() {
  918. if (style) {
  919. return;
  920. }
  921. style = document.createElement('style');
  922. style.innerHTML = policy.createHTML(`
  923. #${ID_PREFIX}-toast-div {
  924. z-index: ${FIRST_Z_INDEX};
  925. position: fixed;
  926. top: 20px;
  927. left: 50%;
  928. transform: translate(-50%, 0);
  929. background-color: rgba(0, 0, 0, 0.8);
  930. color: white;
  931. font-size: 14px;
  932. padding: 10px 20px;
  933. border-radius: 5px;
  934. opacity: 0;
  935. transition: opacity 0.5s ease;
  936. display: none;
  937. letter-spacing: 1px;
  938. }
  939. #${ID_PREFIX}-loading-div {
  940. z-index: ${FIRST_Z_INDEX};
  941. display: none;
  942. position: fixed;
  943. bottom: 50%;
  944. left: 50%;
  945. transform: translate(-50%, -50%);
  946. background-color: rgba(0, 0, 0, 0);
  947. }
  948. #${ID_PREFIX}-loading-div div {
  949. width: 50px;
  950. height: 50px;
  951. background-color: ${COLOR.PRIMARY};
  952. border-radius: 0;
  953. -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out;
  954. animation: sk-rotateplane 1.2s infinite ease-in-out;
  955. }
  956. @-webkit-keyframes sk-rotateplane {
  957. 0% {
  958. -webkit-transform: perspective(120px)
  959. }
  960. 50% {
  961. -webkit-transform: perspective(120px) rotateY(180deg)
  962. }
  963. 100% {
  964. -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg)
  965. }
  966. }
  967. @keyframes sk-rotateplane {
  968. 0% {
  969. transform: perspective(120px) rotateX(0deg) rotateY(0deg);
  970. -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)
  971. }
  972. 50% {
  973. transform: perspective(120px) rotateX(-180deg) rotateY(0deg);
  974. -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(0deg)
  975. }
  976. 100% {
  977. transform: perspective(120px) rotateX(-180deg) rotateY(-180deg);
  978. -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-180deg);
  979. }
  980. }
  981. #${ID_PREFIX}-button-div {
  982. z-index: ${THIRD_Z_INDEX};
  983. position: fixed;
  984. display: none;
  985. align-items: center;
  986. width: auto;
  987. height: auto;
  988. left: ${currentConfig.global.buttonXCoord}px;
  989. bottom: ${currentConfig.global.buttonYCoord}px;
  990. padding: 5px;
  991. border: 3px solid rgba(0, 0, 0, 0);
  992. border-radius: 5px;
  993. cursor: move;
  994. gap: 10px;
  995. background-color: rgba(0, 0, 0, 0);
  996. min-width: ${50 * currentConfig.global.buttonScale}px;
  997. min-height: ${50 * currentConfig.global.buttonScale}px;
  998. }
  999. #${ID_PREFIX}-button-div button {
  1000. color: white;
  1001. font-size: 20px;
  1002. font-weight: bold;
  1003. width: 50px;
  1004. height: 50px;
  1005. outline: none;
  1006. border: none;
  1007. border-radius: 50%;
  1008. cursor: pointer;
  1009. background-size: cover;
  1010. background-color: rgba(0, 0, 0, 0);
  1011. transition: opacity 0.5s ease, visibility 0s linear 0.5s;
  1012. }
  1013. #${ID_PREFIX}-button-div:hover {
  1014. background-color: rgb(255, 255, 255, 0.3) !important;
  1015. }
  1016. #${ID_PREFIX}-button-div:hover button {
  1017. visibility: visible !important;
  1018. transition: opacity 0.5s ease, visibility 0s;
  1019. }
  1020. #${ID_PREFIX}-button-div button:hover {
  1021. transform: scale(1.06);
  1022. box-shadow: 0px 0px 16px #e6e6e6;
  1023. }
  1024. #${ID_PREFIX}-setting-button {
  1025. visibility: hidden;
  1026. position: absolute;
  1027. right: ${-12 * currentConfig.global.buttonScale}px !important;
  1028. top: ${-12 * currentConfig.global.buttonScale}px !important;
  1029. width: ${25 * currentConfig.global.buttonScale}px !important;
  1030. height: ${25 * currentConfig.global.buttonScale}px !important;
  1031. background-image: url('data:image/svg+xml,<svg t="1731846507027" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4281" width="16" height="16"><path d="M616.533333 512.128c0-25.6-9.941333-49.536-28.16-67.669333a95.744 95.744 0 0 0-67.84-28.074667c-25.685333 0-49.706667 9.984-67.925333 28.074667a95.146667 95.146667 0 0 0-28.16 67.669333c0 25.6 10.069333 49.578667 28.16 67.712 18.218667 18.048 42.24 28.074667 67.925333 28.074667 25.642667 0 49.664-10.026667 67.84-28.074667 18.218667-18.133333 28.16-42.112 28.16-67.712z m-202.112 352.896l48-55.978667a309.290667 309.290667 0 0 0 99.029334 0l48 55.978667a27.52 27.52 0 0 0 30.208 7.978667l2.218666-0.768a380.074667 380.074667 0 0 0 118.186667-68.138667l1.834667-1.536a27.434667 27.434667 0 0 0 8.106666-30.037333l-24.746666-69.546667a298.666667 298.666667 0 0 0 49.322666-85.205333l72.874667-13.44a27.477333 27.477333 0 0 0 22.058667-22.101334l0.426666-2.304a384.64 384.64 0 0 0 0-135.936l-0.426666-2.304a27.477333 27.477333 0 0 0-22.058667-22.058666l-73.216-13.525334a302.293333 302.293333 0 0 0-49.194667-84.650666l25.002667-70.016a27.306667 27.306667 0 0 0-8.149333-30.037334l-1.834667-1.536a383.018667 383.018667 0 0 0-118.186667-68.138666l-2.218666-0.768a27.605333 27.605333 0 0 0-30.208 7.936l-48.512 56.661333a302.592 302.592 0 0 0-97.834667 0L414.592 159.146667a27.52 27.52 0 0 0-30.208-7.978667l-2.218667 0.768a381.056 381.056 0 0 0-118.186666 68.138667l-1.834667 1.536a27.434667 27.434667 0 0 0-8.106667 30.037333l24.96 69.973333a296.192 296.192 0 0 0-49.194666 84.693334l-73.216 13.525333a27.477333 27.477333 0 0 0-22.058667 22.058667l-0.426667 2.304a382.592 382.592 0 0 0 0 135.936l0.426667 2.304c2.048 11.221333 10.794667 20.053333 22.058667 22.101333l72.874666 13.44a300.672 300.672 0 0 0 49.365334 85.248l-24.832 69.504a27.306667 27.306667 0 0 0 8.149333 30.037333l1.834667 1.536a383.018667 383.018667 0 0 0 118.186666 68.138667l2.218667 0.768a27.733333 27.733333 0 0 0 30.037333-8.149333z m-44.8-352.853333A150.656 150.656 0 0 1 520.533333 361.642667a150.656 150.656 0 0 1 150.869334 150.442666A150.656 150.656 0 0 1 520.533333 662.613333a150.656 150.656 0 0 1-150.912-150.485333z" fill="${COLOR.PRIMARY}" p-id="4282"></path></svg>');
  1032. }
  1033. #${ID_PREFIX}-setting-iframe {
  1034. z-index: ${SECOND_Z_INDEX};
  1035. position: fixed;
  1036. width: 1000px;
  1037. height: 500px;
  1038. top: 50%;
  1039. left: 50%;
  1040. transform: translate(-50%, -50%);
  1041. border: none;
  1042. border-radius: 5px;
  1043. box-shadow: 0 0 16px rgba(0, 0, 0, 0.6);
  1044. background-color: #fff;
  1045. display: none;
  1046. }
  1047. `);
  1048. document.head.appendChild(style);
  1049. }
  1050.  
  1051. function appendToastDiv() {
  1052. const TOAST_DIV_ID = `${ID_PREFIX}-toast-div`;
  1053. if (document.getElementById(TOAST_DIV_ID)) {
  1054. return;
  1055. }
  1056. toastDiv = document.createElement('div');
  1057. toastDiv.id = TOAST_DIV_ID;
  1058. document.body.appendChild(toastDiv);
  1059. }
  1060.  
  1061. function showToast(message) {
  1062. toastDiv.textContent = message;
  1063. toastDiv.style.opacity = '0.9';
  1064. toastDiv.style.display = 'block';
  1065. setTimeout(() => {
  1066. toastDiv.style.opacity = '0';
  1067. toastDiv.style.display = 'none';
  1068. }, 5000);
  1069. }
  1070.  
  1071. function appendLoadingDiv() {
  1072. const LOADING_DIV_ID = `${ID_PREFIX}-loading-div`;
  1073. if (document.getElementById(LOADING_DIV_ID)) {
  1074. return;
  1075. }
  1076. loadingDiv = document.createElement('div');
  1077. loadingDiv.id = LOADING_DIV_ID;
  1078. loadingDiv.appendChild(document.createElement('div'));
  1079. document.body.appendChild(loadingDiv);
  1080. }
  1081.  
  1082. function showLoading(timeout) {
  1083. if (loadingId) {
  1084. clearTimeout(loadingId);
  1085. loadingId = undefined;
  1086. }
  1087. if (!timeout) {
  1088. timeout = 10000;
  1089. }
  1090. loadingDiv.style.display = 'block';
  1091. loadingId = setTimeout(() => {
  1092. if (loadingDiv.style.display === 'block') {
  1093. hideLoading();
  1094. showToast(translation.loadTimeout);
  1095. }
  1096. }, timeout);
  1097. }
  1098.  
  1099. function hideLoading() {
  1100. loadingDiv.style.display = 'none';
  1101. }
  1102.  
  1103. function appendButtonDiv() {
  1104. const BUTTON_DIV_ID = `${ID_PREFIX}-button-div`;
  1105. if (document.getElementById(BUTTON_DIV_ID)) {
  1106. return;
  1107. }
  1108. buttonDiv = document.createElement('div');
  1109. buttonDiv.id = BUTTON_DIV_ID;
  1110. buttonDiv.addEventListener('mousedown', (e) => {
  1111. if (e.target.tagName === 'BUTTON') {
  1112. return;
  1113. }
  1114. let offsetX = e.clientX - buttonDiv.getBoundingClientRect().left;
  1115. let offsetY = e.clientY - buttonDiv.getBoundingClientRect().top;
  1116.  
  1117. document.addEventListener('mouseup', mouseUpHandler);
  1118. document.addEventListener('mousemove', mouseMoveHandler);
  1119.  
  1120. function mouseUpHandler() {
  1121. buttonDiv.style.border = '3px solid rgba(0, 0, 0, 0)';
  1122. document.removeEventListener('mousemove', mouseMoveHandler);
  1123. document.removeEventListener('mouseup', mouseUpHandler);
  1124. }
  1125.  
  1126. function mouseMoveHandler(e) {
  1127. buttonDiv.style.border = `3px solid ${COLOR.PRIMARY}`;
  1128. let newX = e.clientX - offsetX;
  1129. let newY = e.clientY - offsetY;
  1130.  
  1131. const windowWidth = window.innerWidth;
  1132. const windowHeight = window.innerHeight;
  1133. const divWidth = buttonDiv.offsetWidth;
  1134. const divHeight = buttonDiv.offsetHeight;
  1135.  
  1136. if (newX < 0) newX = 0;
  1137. if (newX + divWidth > windowWidth) newX = windowWidth - divWidth;
  1138. if (newY < 0) newY = 0;
  1139. if (newY + divHeight > windowHeight) newY = windowHeight - divHeight;
  1140.  
  1141. newY = windowHeight - newY - divHeight;
  1142. buttonDiv.style.left = `${newX}px`;
  1143. buttonDiv.style.bottom = `${newY}px`;
  1144. currentConfig.global.buttonXCoord = newX;
  1145. currentConfig.global.buttonYCoord = newY;
  1146. GM_setValue('config', currentConfig);
  1147. }
  1148. });
  1149. document.body.appendChild(buttonDiv);
  1150.  
  1151. appendPlayButton();
  1152. appendSettingButton();
  1153.  
  1154. // 全屏隐藏
  1155. document.addEventListener("fullscreenchange", () => {
  1156. if (document.fullscreenElement) {
  1157. buttonDiv.style.display = "none";
  1158. } else {
  1159. if (currentParser) {
  1160. buttonDiv.style.display = "flex";
  1161. }
  1162. }
  1163. });
  1164. }
  1165.  
  1166. function appendPlayButton() {
  1167. if (!currentConfig.players) {
  1168. return;
  1169. }
  1170. var playButtonNeedAutoClick;
  1171. currentConfig.players.forEach(player => {
  1172. if (player.enable !== true) {
  1173. return;
  1174. }
  1175. const playButton = document.createElement('button');
  1176. if (player.icon) {
  1177. const image = new Image();
  1178. image.src = player.icon;
  1179. image.onload = () => playButton.style.backgroundImage = `url(${image.src})`;
  1180. image.onerror = () => {
  1181. playButton.style.backgroundColor = COLOR.PRIMARY;
  1182. playButton.textContent = player.name ? player.name.substring(0, 1) : 'P';
  1183. };
  1184. } else {
  1185. playButton.style.backgroundColor = COLOR.PRIMARY;
  1186. playButton.textContent = player.name ? player.name.substring(0, 1) : 'P';
  1187. }
  1188. playButton.style.width = `${player.iconSize * currentConfig.global.buttonScale}px`;
  1189. playButton.style.height = `${player.iconSize * currentConfig.global.buttonScale}px`;
  1190.  
  1191. // 自动隐藏
  1192. if (currentConfig.global.buttonVisibilityDuration == 0) {
  1193. playButton.style.visibility = 'hidden';
  1194. } else if (currentConfig.global.buttonVisibilityDuration > 0) {
  1195. setTimeout(() => {
  1196. playButton.style.visibility = 'hidden';
  1197. }, currentConfig.global.buttonVisibilityDuration);
  1198. }
  1199.  
  1200. playButton.addEventListener('click', async function () {
  1201. if (currentParser) {
  1202. currentParser.play(player);
  1203. } else {
  1204. showToast(translation.noMatchingParserFound);
  1205. }
  1206. });
  1207.  
  1208. buttonDiv.appendChild(playButton);
  1209. });
  1210. }
  1211.  
  1212. function appendSettingButton() {
  1213. settingButton = document.createElement('button');
  1214. settingButton.id = `${ID_PREFIX}-setting-button`;
  1215. settingButton.title = 'Ctrl + Alt + E';
  1216.  
  1217. settingButton.addEventListener('click', async () => {
  1218. await appendSettingIframe();
  1219. if (settingIframe.style.display === "block") {
  1220. settingIframe.style.display = "none";
  1221. } else {
  1222. settingIframe.contentWindow.postMessage({
  1223. defaultConfig: defaultConfig,
  1224. config: currentConfig
  1225. }, '*');
  1226. settingIframe.style.display = "block";
  1227. }
  1228. });
  1229. buttonDiv.appendChild(settingButton);
  1230.  
  1231. // 失去焦点隐藏设置页面
  1232. document.addEventListener('click', (event) => {
  1233. if (settingIframe && settingIframe.style.display === 'block' &&
  1234. !settingButton.contains(event.target) &&
  1235. !settingIframe.contains(event.target)) {
  1236. settingIframe.style.display = 'none';
  1237. }
  1238. });
  1239. }
  1240.  
  1241. async function appendSettingIframe() {
  1242. const SETTING_IFRAME_ID = `${ID_PREFIX}-setting-iframe`;
  1243. if (document.getElementById(SETTING_IFRAME_ID)) {
  1244. return;
  1245. }
  1246. settingIframe = document.createElement('iframe');
  1247. settingIframe.id = SETTING_IFRAME_ID;
  1248. let settingIframeHtml = `
  1249. <!DOCTYPE html>
  1250. <html lang="en">
  1251.  
  1252. <head>
  1253. <meta charset="UTF-8">
  1254. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1255. <title>External Player</title>
  1256. <style>
  1257. :root {
  1258. --primary-color: ${COLOR.PRIMARY};
  1259. --text-color: ${COLOR.TEXT};
  1260. --text-active-color: ${COLOR.TEXT_ACTIVE};
  1261. --warning-color: ${COLOR.WARNING};
  1262. --border-color: ${COLOR.BORDER};
  1263. }
  1264.  
  1265. body {
  1266. display: flex;
  1267. flex-direction: row;
  1268. height: 100vh;
  1269. margin: 0;
  1270. }
  1271.  
  1272. body,
  1273. button,
  1274. input,
  1275. textarea,
  1276. select {
  1277. font-family: auto;
  1278. color: var(--text-color);
  1279. }
  1280.  
  1281. ::placeholder {
  1282. font-family: auto;
  1283. color: var(--text-color);
  1284. opacity: 0.2;
  1285. }
  1286.  
  1287. #sidebar-container {
  1288. display: none;
  1289. flex: 0 0 200px;
  1290. flex-direction: column;
  1291. background-color: #f4f4f4;
  1292. box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
  1293. padding: 25px 20px 35px 20px;
  1294. }
  1295.  
  1296. #sidebar {
  1297. flex: 1;
  1298. overflow-y: auto;
  1299. position: relative;
  1300. border: none;
  1301. border-radius: 5px;
  1302. margin-bottom: 10px;
  1303. }
  1304.  
  1305. #sidebar::-webkit-scrollbar {
  1306. display: none !important;
  1307. }
  1308.  
  1309. .reset-button,
  1310. #add-tab-button,
  1311. #save-button,
  1312. #sidebar button {
  1313. width: 200px;
  1314. padding: 10px;
  1315. margin: 0 0 10px 0;
  1316. border: none;
  1317. border-radius: 5px;
  1318. background-color: #e0e0e0;
  1319. cursor: pointer;
  1320. font-size: 15px;
  1321. white-space: nowrap;
  1322. display: inline-flex;
  1323. position: relative;
  1324. align-items: center;
  1325. justify-content: center;
  1326. }
  1327.  
  1328. #add-tab-button,
  1329. #save-button {
  1330. background-color: var(--primary-color);
  1331. color: var(--text-active-color);
  1332. margin: 0;
  1333. }
  1334.  
  1335. #add-tab-button {
  1336. font-size: 25px;
  1337. line-height: 21.45px;
  1338. }
  1339.  
  1340. #add-tab-button:hover,
  1341. #save-button:hover {
  1342. opacity: 0.9;
  1343. }
  1344.  
  1345. #reset-button-coord-button {
  1346. padding: 7px 10px;
  1347. }
  1348.  
  1349. .reset-button {
  1350. margin: 0;
  1351. width: 80px;
  1352. background-color: var(--warning-color);
  1353. color: var(--text-active-color);
  1354. opacity: 0.6;
  1355. }
  1356.  
  1357. .reset-button:hover {
  1358. opacity: 0.8;
  1359. }
  1360.  
  1361. #sidebar button svg {
  1362. width: 20px !important;
  1363. height: 20px !important;
  1364. position: absolute;
  1365. left: 10px;
  1366. fill: var(--text-color);
  1367. }
  1368.  
  1369. #content .radio-button svg {
  1370. width: 20px !important;
  1371. height: 20px !important;
  1372. fill: var(--text-color);
  1373. }
  1374.  
  1375. #sidebar button.active svg,
  1376. #sidebar button:hover svg,
  1377. #content .radio-button.active svg,
  1378. #content .radio-button:hover svg {
  1379. fill: var(--text-active-color)
  1380. }
  1381.  
  1382. #sidebar button.active {
  1383. background-color: var(--primary-color);
  1384. color: var(--text-active-color);
  1385. }
  1386.  
  1387. #sidebar button:hover {
  1388. background-color: var(--primary-color);
  1389. color: var(--text-active-color);
  1390. }
  1391.  
  1392. #content-container {
  1393. display: none;
  1394. flex-direction: column;
  1395. flex: 1;
  1396. padding: 25px 20px 0 20px;
  1397. }
  1398.  
  1399. #content {
  1400. flex: 1;
  1401. padding: 20px;
  1402. overflow-y: auto;
  1403. position: relative;
  1404. border: 1px solid var(--border-color);
  1405. border-radius: 5px;
  1406. margin-bottom: 15px;
  1407. }
  1408.  
  1409. .tab {
  1410. display: none;
  1411. position: relative;
  1412. }
  1413.  
  1414. .tab.active {
  1415. display: block;
  1416. }
  1417.  
  1418. .input-group {
  1419. margin-bottom: 15px;
  1420. }
  1421.  
  1422. label {
  1423. display: flex;
  1424. margin-bottom: 5px;
  1425. font-weight: bold;
  1426. align-items: center;
  1427. }
  1428.  
  1429. input[type="number"] {
  1430. width: calc(100% - 16px);
  1431. font-size: 14px;
  1432. border-radius: 5px;
  1433. border: 1px solid var(--border-color);
  1434. margin-right: 15px;
  1435. padding: 8px;
  1436. }
  1437.  
  1438. input[type="text"],
  1439. input[type="search"],
  1440. textarea {
  1441. width: 100%;
  1442. min-width: 400px;
  1443. padding: 8px;
  1444. border: 1px solid var(--border-color);
  1445. border-radius: 5px;
  1446. font-size: 14px;
  1447. box-sizing: border-box;
  1448. }
  1449.  
  1450. textarea {
  1451. resize: vertical;
  1452. height: 160px;
  1453. }
  1454.  
  1455. .switch {
  1456. position: relative;
  1457. display: inline-block;
  1458. width: 54px;
  1459. height: 24px;
  1460. }
  1461.  
  1462. .switch input {
  1463. opacity: 0;
  1464. width: 0;
  1465. height: 0;
  1466. }
  1467.  
  1468. .switch-slider {
  1469. position: absolute;
  1470. cursor: pointer;
  1471. top: 0;
  1472. left: 0;
  1473. right: 0;
  1474. bottom: 0;
  1475. background-color: #ccc;
  1476. transition: 0.4s;
  1477. border-radius: 34px;
  1478. }
  1479.  
  1480. .switch-slider:before {
  1481. position: absolute;
  1482. content: "";
  1483. height: 16px;
  1484. width: 16px;
  1485. border-radius: 50%;
  1486. left: 4px;
  1487. bottom: 4px;
  1488. background-color: var(--text-active-color);
  1489. transition: 0.4s;
  1490. }
  1491.  
  1492. input:checked+.switch-slider {
  1493. background-color: var(--primary-color);
  1494. }
  1495.  
  1496. input:checked+.switch-slider:before {
  1497. transform: translateX(30px);
  1498. }
  1499.  
  1500. .remove-button {
  1501. position: absolute;
  1502. opacity: 0.9;
  1503. top: -10px;
  1504. right: 0;
  1505. background: var(--warning-color);
  1506. color: var(--text-active-color);
  1507. border: none;
  1508. padding: 5px 10px;
  1509. cursor: pointer;
  1510. border-radius: 5px;
  1511. font-size: 14px;
  1512. }
  1513.  
  1514. .remove-button:hover {
  1515. opacity: 1;
  1516. }
  1517.  
  1518. .radio-button-group,
  1519. .checkbox-group {
  1520. display: flex;
  1521. flex-wrap: wrap;
  1522. gap: 10px;
  1523. margin-bottom: 15px;
  1524. }
  1525.  
  1526. .radio-button,
  1527. .checkbox-group .chekbox-label {
  1528. padding: 8px 23.5px;
  1529. background-color: #e0e0e0;
  1530. cursor: pointer;
  1531. border-radius: 5px;
  1532. font-size: 14px;
  1533. font-weight: normal;
  1534. min-width: 100px;
  1535. display: inline-flex;
  1536. justify-content: center;
  1537. align-items: center;
  1538. gap: 10px;
  1539. height: 20px;
  1540. margin: 0;
  1541. }
  1542.  
  1543. .radio-button.active,
  1544. .checkbox-group input:checked+.chekbox-label {
  1545. background-color: var(--primary-color);
  1546. color: var(--text-active-color);
  1547. }
  1548.  
  1549. .radio-button:hover {
  1550. background-color: var(--primary-color);
  1551. color: var(--text-active-color);
  1552. }
  1553.  
  1554. .checkbox-group input[type="checkbox"] {
  1555. display: none;
  1556. }
  1557.  
  1558. #language {
  1559. padding: 8px;
  1560. border-radius: 5px;
  1561. cursor: pointer;
  1562. width: 100%;
  1563. border: 1px solid var(--border-color);
  1564. }
  1565.  
  1566. .parser {
  1567. border: 1px solid var(--border-color);
  1568. border-radius: 5px;
  1569. padding: 10px 20px;
  1570. }
  1571.  
  1572. .parser textarea {
  1573. margin-bottom: 10px;
  1574. resize: none;
  1575. }
  1576.  
  1577. a {
  1578. color: var(--text-color);
  1579. text-decoration: none;
  1580. font-weight: bold;
  1581. transition: color 0.3s ease, border-bottom 0.3s ease;
  1582. border-bottom: 2px solid transparent;
  1583. }
  1584.  
  1585. a:hover {
  1586. color: var(--primary-color);
  1587. border-bottom-color: var(--primary-color);
  1588. }
  1589.  
  1590. #tab-container {
  1591. flex: 1;
  1592. padding: 20px;
  1593. overflow-y: auto;
  1594. position: relative;
  1595. border: 1px solid var(--border-color);
  1596. border-radius: 5px;
  1597. margin-bottom: 10px;
  1598. }
  1599.  
  1600. :disabled {
  1601. opacity: 0.6;
  1602. }
  1603.  
  1604. div.disabled,
  1605. button:disabled {
  1606. pointer-events: none !important;
  1607. cursor: not-allowed !important;
  1608. }
  1609.  
  1610. .parser textarea:disabled {
  1611. height: 30px;
  1612. overflow-y: hidden;
  1613. line-height: 20px;
  1614. }
  1615.  
  1616. textarea:disabled::-webkit-scrollbar {
  1617. display: none;
  1618. }
  1619.  
  1620. ::-webkit-scrollbar {
  1621. width: 19px !important;
  1622. height: 19px !important;
  1623. }
  1624.  
  1625. ::-webkit-scrollbar-thumb {
  1626. background: var(--border-color) !important;
  1627. border-radius: 5px !important;
  1628. }
  1629.  
  1630. ::-webkit-scrollbar-thumb:hover {
  1631. background: var(--primary-color) !important;
  1632. }
  1633.  
  1634. ::-webkit-scrollbar-track {
  1635. background: rgb(245, 245, 245) !important;
  1636. border-radius: 5px !important;
  1637. }
  1638.  
  1639. select:focus,
  1640. input:focus,
  1641. textarea:focus {
  1642. border-color: var(--primary-color);
  1643. outline: none;
  1644. }
  1645.  
  1646. #footer {
  1647. font-size: 14px;
  1648. height: 35px;
  1649. display: flex;
  1650. justify-content: center;
  1651. align-items: center;
  1652. }
  1653.  
  1654. #footer svg {
  1655. width: 20px;
  1656. height: 20px;
  1657. margin-bottom: -3px;
  1658. }
  1659.  
  1660. #footer a,
  1661. #footer a:hover {
  1662. margin-left: 3px;
  1663. margin-right: 3px;
  1664. font-weight: normal;
  1665. border-bottom: none !important;
  1666. text-decoration: none !important;
  1667. }
  1668. </style>
  1669. </head>
  1670.  
  1671. <body>
  1672. <div id="sidebar-container">
  1673. <div id="sidebar">
  1674. <button id="global-button" class="tab-button active" data-tab="global">
  1675. <svg t="1732015880724" class="icon" viewBox="0 0 1024 1024" version="1.1"
  1676. xmlns="http://www.w3.org/2000/svg" p-id="4317" width="32" height="32">
  1677. <path
  1678. d="M386.35 112.05h-228.7c-25.2 0-45.7 20.5-45.7 45.7v228.5c0 25.2 20.4 45.7 45.6 45.8h228.6c25.2 0 45.7-20.4 45.8-45.6V157.65c0.1-25.2-20.4-45.6-45.6-45.6z"
  1679. p-id="4318"></path>
  1680. <path
  1681. d="M157.55 80.05h229c42.8 0 77.5 34.7 77.5 77.5v229c0 42.8-34.7 77.5-77.5 77.5h-229c-42.8 0-77.5-34.7-77.5-77.5v-229c0-42.8 34.7-77.5 77.5-77.5z m228.9 320.5c7.8 0 14.1-6.3 14.1-14.1v-229c0-7.8-6.3-14.1-14.1-14.1h-229c-7.8 0-14.1 6.3-14.1 14.1v229c0 7.8 6.3 14.1 14.1 14.1h229z"
  1682. p-id="4319"></path>
  1683. <path
  1684. d="M387.55 590.25h-231.1c-25.5 0-46.2 20.7-46.2 46.2v231.1c0 25.5 20.7 46.2 46.2 46.2h231.1c25.5 0 46.2-20.7 46.2-46.2v-231.1c0-25.5-20.7-46.2-46.2-46.2z"
  1685. p-id="4320"></path>
  1686. <path
  1687. d="M157.55 560.05h229c42.8 0 77.5 34.7 77.5 77.5v229c0 42.8-34.7 77.5-77.5 77.5h-229c-42.8 0-77.5-34.7-77.5-77.5v-229c0-42.8 34.7-77.5 77.5-77.5z m228.9 320.5c7.8 0 14.1-6.3 14.1-14.1v-229c0-7.8-6.3-14.1-14.1-14.1h-229c-7.8 0-14.1 6.3-14.1 14.1v229c0 7.8 6.3 14.1 14.1 14.1h229zM637.55 80.05h229c42.8 0 77.5 34.7 77.5 77.5v229c0 42.8-34.7 77.5-77.5 77.5h-229c-42.8 0-77.5-34.7-77.5-77.5v-229c0-42.8 34.7-77.5 77.5-77.5z m228.9 320.5c7.8 0 14.1-6.3 14.1-14.1v-229c0-7.8-6.3-14.1-14.1-14.1h-229c-7.8 0-14.1 6.3-14.1 14.1v229c0 7.8 6.3 14.1 14.1 14.1h229z"
  1688. p-id="4321"></path>
  1689. <path
  1690. d="M866.306 592.006h-228.6c-25.2 0-45.7 20.5-45.7 45.7v228.5c0 25.2 20.5 45.7 45.7 45.7h228.5c25.2 0 45.7-20.4 45.8-45.6v-228.6c0-25.2-20.5-45.7-45.7-45.7z"
  1691. p-id="4322"></path>
  1692. <path
  1693. d="M637.506 560.006h229c42.8 0 77.5 34.7 77.5 77.5v229c0 42.8-34.7 77.5-77.5 77.5h-229c-42.8 0-77.5-34.7-77.5-77.5v-229c0-42.8 34.7-77.5 77.5-77.5z m229 320.6c7.8 0 14.1-6.3 14.1-14.1v-229c0-7.8-6.3-14.1-14.1-14.1h-229c-7.8 0-14.1 6.3-14.1 14.1v229c0 7.8 6.3 14.1 14.1 14.1h229z"
  1694. p-id="4323"></path>
  1695. </svg>
  1696. <span data-translate="global">全局配置</span>
  1697. </button>
  1698. </div>
  1699. <button id="add-tab-button">+</button>
  1700. </div>
  1701.  
  1702. <div id="content-container">
  1703. <div id="content">
  1704. <div id="global" class="tab active">
  1705. <div class="input-group">
  1706. <label data-translate="version">版本</label>
  1707. <input type="text" id="version" readonly></input>
  1708. </div>
  1709. <div class="input-group">
  1710. <label data-translate="language">语言</label>
  1711. <select id="language">
  1712. <option value="zh" selected>中文</option>
  1713. <option value="en">English</option>
  1714. </select>
  1715. </div>
  1716. <div class="input-group">
  1717. <label data-translate="buttonCoord">按钮坐标</label>
  1718. <label>
  1719. <input type="number" id="buttonXCoord" min="0" placeholder="0">
  1720. <input type="number" id="buttonYCoord" min="0" placeholder="0">
  1721. <button id="reset-button-coord-button" class="reset-button" data-translate="reset">重置</button>
  1722. </label>
  1723. </div>
  1724. <div class="input-group">
  1725. <label data-translate="buttonScale">按钮比例</label>
  1726. <input type="number" id="buttonScale" min="0.01" max="10" step="0.01" placeholder="1.00">
  1727. </div>
  1728. <div class="input-group">
  1729. <label data-translate="buttonVisibilityDuration">按钮可见时长(毫秒,-1:一直可见)</label>
  1730. <input type="number" id="buttonVisibilityDuration" min="-1" placeholder="3000">
  1731. </div>
  1732. <div class="input-group">
  1733. <label data-translate="networkProxy">网络代理</label>
  1734. <input type="text" id="networkProxy" placeholder="http://127.0.0.1:7890"></input>
  1735. </div>
  1736. <label data-translate="parser">解析器</label>
  1737. <div class="input-group parser" id="ytdlp">
  1738. <label><a href="https://github.com/yt-dlp/yt-dlp" target="_blank">YTDLP</a></label>
  1739. <textarea name="regex" disabled></textarea>
  1740. <textarea name="regex"></textarea>
  1741. <label data-translate="preferredQuality">首选画质</label>
  1742. <div class="radio-button-group" name="preferredQuality">
  1743. <div class="radio-button active" value="unlimited" data-translate="unlimited">无限制</div>
  1744. <div class="radio-button" value="2160">2160P</div>
  1745. <div class="radio-button" value="1440">1440P</div>
  1746. <div class="radio-button" value="1080">1080P</div>
  1747. <div class="radio-button" value="720">720P</div>
  1748. </div>
  1749. </div>
  1750. <div class="input-group parser" id="video">
  1751. <label><a href="https://github.com/LuckyPuppy514/external-player" target="_blank">VIDEO</a></label>
  1752. <textarea name="regex" disabled></textarea>
  1753. <textarea name="regex"></textarea>
  1754. </div>
  1755. <div class="input-group parser" id="url">
  1756. <label><a href="https://github.com/LuckyPuppy514/external-player" target="_blank">URL</a></label>
  1757. <textarea name="regex" disabled></textarea>
  1758. <textarea name="regex"></textarea>
  1759. </div>
  1760. <div class="input-group parser" id="html">
  1761. <label><a href="https://github.com/LuckyPuppy514/external-player" target="_blank">HTML</a></label>
  1762. <textarea name="regex" disabled></textarea>
  1763. <textarea name="regex"></textarea>
  1764. </div>
  1765. <div class="input-group parser" id="script">
  1766. <label><a href="https://github.com/LuckyPuppy514/external-player" target="_blank">SCRIPT</a></label>
  1767. <textarea name="regex" disabled></textarea>
  1768. <textarea name="regex"></textarea>
  1769. </div>
  1770. <div class="input-group parser" id="request">
  1771. <label><a href="https://github.com/LuckyPuppy514/external-player"
  1772. target="_blank">REQUEST</a></label>
  1773. <textarea name="regex" disabled></textarea>
  1774. <textarea name="regex"></textarea>
  1775. </div>
  1776. <div class="input-group parser" id="bilibili">
  1777. <label><a href="https://github.com/SocialSisterYi/bilibili-API-collect"
  1778. target="_blank">BILIBILI</a></label>
  1779. <textarea name="regex" disabled></textarea>
  1780. <textarea name="regex" style="display: none;"></textarea>
  1781. <label data-translate="preferredQuality">首选画质</label>
  1782. <div class="radio-button-group" name="preferredQuality">
  1783. <div class="radio-button active" value="127" data-translate="unlimited">无限制</div>
  1784. <div class="radio-button" value="126">2160P</div>
  1785. <div class="radio-button" value="116">1080P</div>
  1786. <div class="radio-button" value="74">720P</div>
  1787. </div>
  1788. <label data-translate="preferredSubtitle">首选字幕</label>
  1789. <div class="radio-button-group" name="preferredSubtitle">
  1790. <div class="radio-button active" value="off" data-translate="off">关闭</div>
  1791. <div class="radio-button" value="zh-Hans">简体</div>
  1792. <div class="radio-button" value="zh-Hant">繁体</div>
  1793. <div class="radio-button" value="en-US">English</div>
  1794. </div>
  1795. <label data-translate="preferredCodec">首选编码</label>
  1796. <div class="radio-button-group" name="preferredCodec">
  1797. <div class="radio-button active" value="12">HEVC</div>
  1798. <div class="radio-button" value="13">AV1</div>
  1799. <div class="radio-button" value="7">AVC</div>
  1800. </div>
  1801. </div>
  1802. <div class="input-group parser" id="bilibiliLive">
  1803. <label><a href="https://github.com/SocialSisterYi/bilibili-API-collect" target="_blank">BILIBILI
  1804. LIVE</a></label>
  1805. <textarea name="regex" disabled></textarea>
  1806. <textarea name="regex" style="display: none;"></textarea>
  1807. <label data-translate="preferredQuality">首选画质</label>
  1808. <div class="radio-button-group" name="preferredQuality">
  1809. <div class="radio-button active" value="4" data-translate="original">原画</div>
  1810. <div class="radio-button active" value="3" data-translate="hd">高清</div>
  1811. <div class="radio-button active" value="2" data-translate="smooth">流畅</div>
  1812. </div>
  1813. <label data-translate="preferredLine">首选线路</label>
  1814. <div class="radio-button-group" name="preferredLine">
  1815. <div class="radio-button active" value="0" data-translate="mainLine">主线</div>
  1816. <div class="radio-button active" value="1" data-translate="backupLine1">备线1</div>
  1817. <div class="radio-button active" value="2" data-translate="backupLine2">备线2</div>
  1818. <div class="radio-button active" value="3" data-translate="backupLine3">备线3</div>
  1819. </div>
  1820. </div>
  1821. </div>
  1822. </div>
  1823.  
  1824. <div style="margin: 0 auto;">
  1825. <button id="save-button" data-translate="save">保存</button>
  1826. <button id="reset-button" class="reset-button" data-translate="reset">重置</button>
  1827. </div>
  1828.  
  1829. <div id="footer">
  1830. <span>
  1831. <a href="https://github.com/LuckyPuppy514" target="_blank">
  1832. &copy 2024 LuckyPuppy514
  1833. </a>
  1834. <svg t="1731923678389" class="icon" viewBox="0 0 1024 1024" version="1.1"
  1835. xmlns="http://www.w3.org/2000/svg" p-id="5894" width="32" height="32">
  1836. <path
  1837. d="M20.48 503.72608c0 214.4256 137.4208 396.73856 328.94976 463.6672 25.8048 6.5536 21.87264-11.8784 21.87264-24.33024v-85.07392c-148.93056 17.44896-154.86976-81.1008-164.94592-97.52576-20.23424-34.52928-67.91168-43.33568-53.69856-59.76064 33.91488-17.44896 68.48512 4.42368 108.46208 63.61088 28.95872 42.88512 85.44256 35.6352 114.15552 28.4672a138.8544 138.8544 0 0 1 38.0928-66.7648c-154.25536-27.60704-218.60352-121.77408-218.60352-233.79968 0-54.31296 17.94048-104.2432 53.0432-144.54784-22.36416-66.43712 2.08896-123.24864 5.3248-131.6864 63.81568-5.7344 130.00704 45.6704 135.168 49.68448 36.2496-9.78944 77.57824-14.9504 123.82208-14.9504 46.4896 0 88.064 5.3248 124.5184 15.23712 12.288-9.4208 73.80992-53.53472 133.12-48.128 3.15392 8.43776 27.0336 63.93856 6.02112 129.4336 35.59424 40.38656 53.69856 90.76736 53.69856 145.24416 0 112.18944-64.7168 206.4384-219.42272 233.71776a140.0832 140.0832 0 0 1 41.7792 99.9424v123.4944c0.86016 9.87136 0 19.6608 16.50688 19.6608 194.31424-65.49504 334.2336-249.15968 334.2336-465.5104C1002.57792 232.48896 782.66368 12.77952 511.5904 12.77952 240.18944 12.65664 20.48 232.40704 20.48 503.72608z"
  1838. fill="#000000" opacity=".65" p-id="5895"></path>
  1839. </svg>
  1840. <a href="https://github.com/LuckyPuppy514/external-player" target="_blank">
  1841. Powered by External Player
  1842. </a>
  1843. </span>
  1844. </div>
  1845. </div>
  1846. </body>
  1847. <script>
  1848. const translations = {
  1849. en: {
  1850. global: 'Global Config',
  1851. version: 'Version',
  1852. language: 'Language',
  1853. buttonCoord: 'Button Coord',
  1854. buttonScale: 'Button Scale',
  1855. buttonVisibilityDuration: 'Button Visibility Duration (ms, -1: Keep Visible)',
  1856. networkProxy: 'Network Proxy',
  1857. reset: 'Reset',
  1858. save: 'Save',
  1859. delete: 'Delete',
  1860. name: 'Name',
  1861. system: 'System',
  1862. icon: 'Icon',
  1863. iconSize: 'Icon Size',
  1864. playEvent: 'Play Event',
  1865. enable: 'Enable',
  1866. parser: 'Parser',
  1867. preferredQuality: 'Preferred Quality',
  1868. preferredSubtitle: 'Preferred Subtitle',
  1869. preferredCodec: 'Preferred Codec',
  1870. preferredLine: 'Preferred Line',
  1871. original: 'Original',
  1872. hd: 'HD',
  1873. smooth: 'Smooth',
  1874. mainLine: 'Main',
  1875. backupLine1: 'Backup 1',
  1876. backupLine2: 'Backup 2',
  1877. backupLine3: 'Backup 3',
  1878. unlimited: 'Unlimited',
  1879. off: 'OFF'
  1880. },
  1881. zh: {
  1882. global: '全局配置',
  1883. version: '版本',
  1884. language: '语言',
  1885. buttonCoord: '按钮坐标',
  1886. buttonScale: '按钮比例',
  1887. buttonVisibilityDuration: '按钮可见时长(毫秒,-1:一直可见)',
  1888. networkProxy: '网络代理',
  1889. reset: '重置',
  1890. save: '保存',
  1891. delete: '删除',
  1892. name: '名称',
  1893. system: '系统',
  1894. icon: '图标',
  1895. iconSize: '图标大小',
  1896. playEvent: '播放事件',
  1897. enable: '启用',
  1898. parser: '解析器',
  1899. preferredQuality: '首选画质',
  1900. preferredSubtitle: '首选字幕',
  1901. preferredCodec: '首选编码',
  1902. preferredLine: '首选线路',
  1903. original: '原画',
  1904. hd: '高清',
  1905. smooth: '流畅',
  1906. mainLine: '主线',
  1907. backupLine1: '备线1',
  1908. backupLine2: '备线2',
  1909. backupLine3: '备线3',
  1910. unlimited: '无限制',
  1911. off: '关闭',
  1912. }
  1913. };
  1914.  
  1915. const SYSTEM_SVG = {
  1916. windows: '<svg t="1732017849573" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5376" width="32" height="32"><path d="M523.8 191.4v288.9h382V128.1zM523.8 833.6l382 62.2v-352h-382zM120.1 480.2H443V201.9l-322.9 53.5zM120.1 770.6L443 823.2V543.8H120.1z" p-id="5377"></path></svg>',
  1917. linux: '<svg t="1732017810402" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4326" width="32" height="32"><path d="M834.198588 918.588235c-30.659765 15.661176-71.559529 50.115765-86.618353 64.572236-11.324235 10.782118-58.066824 16.203294-84.449882 2.710588-30.659765-15.661176-14.516706-40.417882-61.861647-41.923765-23.672471-0.602353-46.802824-0.602353-69.933177-0.602353-20.419765 0.602353-40.839529 1.626353-61.861647 2.108235-70.957176 1.626353-77.944471 47.405176-123.723294 45.778824-31.201882-1.084235-70.415059-25.840941-138.24-39.755294-47.344941-9.758118-93.003294-12.348235-102.761412-33.370353-9.637647-21.022118 11.866353-44.634353 13.432471-65.054118 1.626353-27.467294-20.419765-64.572235-4.276706-78.607059 13.974588-12.348235 43.550118-3.252706 62.885647-13.914352 20.419765-11.806118 29.033412-21.022118 29.033412-46.260706 7.529412 25.720471-0.542118 46.682353-17.227294 56.922353-10.24 6.445176-29.033412 9.697882-44.694588 8.131764-12.348235-1.144471-19.877647 0.481882-23.130353 5.360941-4.818824 5.903059-3.252706 16.685176 2.710588 30.659765 5.903059 13.974588 12.890353 23.130353 11.806118 40.297412-0.542118 17.227294-19.877647 37.707294-16.624942 52.224 1.084235 5.421176 6.445176 10.24 19.877647 13.974588 21.504 5.903059 60.777412 11.806118 98.966589 21.022118 42.526118 10.721882 86.618353 30.057412 114.085647 26.322823 81.739294-11.324235 34.936471-98.966588 22.046117-119.868235-69.391059-108.724706-115.109647-179.681882-151.67247-151.732706-9.155765 7.529412-9.697882-18.311529-9.155765-28.551529 1.626353-35.538824 19.395765-48.368941 30.117647-75.836236 20.419765-52.224 36.020706-111.856941 67.222588-142.516705 23.311059-30.177882 59.873882-79.088941 66.921412-104.869647-5.963294-55.958588-7.589647-115.109647-8.613647-166.671059-1.084235-55.416471 7.529412-103.905882 69.933177-137.697883C453.391059 33.310118 473.268706 30.117647 494.290824 30.117647c37.104941-0.602353 78.486588 10.24 104.869647 29.575529 41.984 31.201882 68.306824 97.340235 65.114353 144.624942-2.168471 37.104941 4.276706 75.294118 16.143058 115.109647 13.974588 46.802824 36.080941 79.570824 71.55953 117.217882 42.526118 45.176471 75.836235 133.903059 85.534117 190.343529 8.613647 52.826353-3.252706 85.594353-14.516705 87.220706-17.227294 2.590118-27.949176 56.922353-81.739295 54.814118-34.394353-1.626353-37.647059-22.046118-47.344941-39.815529-15.600941-27.407059-31.201882-18.793412-37.104941 10.24-3.252706 14.516706-1.144471 36.080941 3.734588 52.103529 9.697882 33.912471 6.445176 65.656471 0.542118 104.929882-11.324235 74.209882 52.163765 88.184471 94.689882 52.645647 41.923765-34.876235 51.079529-40.297412 103.785412-58.608941 80.112941-27.467294 53.248-51.621647 10.179765-66.138353-38.731294-12.950588-40.297412-78.064941-26.383059-90.413176 3.252706 69.933176 39.815529 80.173176 54.874353 89.810823 66.138353 41.020235-24.756706 74.932706-64.030118 94.810353z m-90.352941-259.734588c14.516706-48.489412 8.071529-67.764706-1.566118-113.543529-7.529412-34.394353-39.273412-81.257412-64.030117-95.713883 6.445176 5.360941 18.311529 20.961882 30.659764 44.574118 21.504 40.417882 43.008 100.050824 29.033412 149.564235-5.360941 19.275294-18.251294 21.985882-26.864941 22.528-37.647059 4.336941-15.600941-45.176471-31.201882-112.338823-17.769412-75.354353-36.020706-80.715294-40.297412-86.618353-22.166588-97.822118-46.320941-88.124235-53.368471-124.687059-5.903059-32.828235 28.551529-59.693176-18.251294-68.848941-14.516706-2.710588-34.936471-17.227294-43.008-18.31153-8.071529-1.024-12.408471-54.332235 17.709177-55.958588 29.575529-2.168471 34.996706 33.370353 29.575529 47.405177-8.553412 13.914353 0.542118 19.335529 15.119059 14.45647 11.806118-3.734588 4.276706-34.936471 6.987294-39.213176-7.529412-45.176471-26.383059-51.621647-45.718588-55.416471-74.270118 5.903059-40.899765 87.702588-48.429177 80.173177-10.782118-11.324235-41.923765-1.084235-41.923764-8.131765 0.542118-41.923765-13.492706-66.138353-32.828236-66.680471-21.504-0.542118-30.117647 29.575529-31.201882 46.742589-1.626353 16.143059 9.155765 50.115765 17.227294 47.405176 5.360941-1.626353 14.516706-12.408471 4.818824-11.806118-4.818824 0-12.348235-11.866353-13.432471-25.840941-0.542118-14.034824 4.879059-28.009412 23.130353-27.467294 20.961882 0.542118 20.961882 42.465882 18.793412 44.092235-6.927059 4.818824-15.600941 14.034824-16.685177 15.600942-6.927059 11.324235-20.359529 14.456471-25.780706 19.395764-9.155765 9.637647-11.264 20.419765-4.276705 24.154353 24.696471 13.974588 16.624941 30.057412 51.079529 31.262118 22.588235 1.084235 39.213176-3.252706 54.874353-8.07153 11.806118-3.734588 50.055529-11.806118 58.066823-25.840941 3.734588-5.903059 8.071529-5.903059 10.721883-4.276706 5.360941 2.650353 6.445176 12.890353-6.987294 16.143059-18.793412 5.421176-37.647059 15.661176-54.814118 22.106353-16.685176 6.927059-22.046118 9.637647-37.647059 12.288-35.478588 6.445176-61.801412-12.890353-38.189176 10.24 8.071529 7.529412 15.600941 12.348235 36.020706 11.866353 45.176471-1.626353 95.232-56.018824 100.050823-31.804235 1.024 5.360941-14.034824 11.806118-25.840941 17.769412-41.923765 20.419765-71.499294 61.319529-98.424471 47.284705-24.214588-12.890353-48.368941-72.643765-47.887058-45.658353 0.542118 41.381647-54.332235 77.944471-29.033412 125.289412-16.685176 4.216471-53.790118 83.365647-59.151059 124.205177-3.252706 23.672471 2.168471 52.705882-3.794824 68.848941-8.071529 23.672471-44.634353-22.588235-32.768-79.028706 2.108235-9.637647 0-11.866353-2.710588-6.927059-14.516706 26.322824-6.445176 63.427765 5.360941 89.208471 4.879059 11.324235 17.227294 16.143059 26.383059 25.840941 18.793412 21.443765 93.003294 76.378353 105.953883 89.810823a33.008941 33.008941 0 0 1-22.588236 55.898353c17.769412 33.370353 34.936471 36.623059 34.454588 90.895059 20.419765-10.721882 12.408471-34.394353 3.734589-49.392941-5.963294-10.842353-13.432471-15.661176-11.866353-18.311529 1.084235-1.626353 11.866353-10.842353 17.769412-3.734589 18.251294 20.419765 52.705882 24.154353 89.268705 19.33553 37.104941-4.336941 76.920471-17.227294 95.171765-46.802824 8.613647-13.974588 14.516706-18.793412 18.31153-16.143059 4.276706 2.108235 5.963294 11.806118 5.360941 27.949177-0.542118 17.227294-7.529412 34.996706-12.348236 49.513412-4.879059 16.685176-6.445176 27.949176 9.697883 28.551529 4.276706-30.177882 12.890353-59.753412 15.058823-89.871059 2.710588-34.394353-22.046118-97.822118 4.879059-129.626353 6.987294-8.613647 15.540706-9.637647 27.407059-9.637647 1.566118-43.068235 67.764706-39.755294 89.810823-22.046117 0-9.758118-20.961882-18.853647-29.575529-22.648471zM304.971294 503.988706c-3.794824 6.927059-13.432471 12.288-5.963294 13.43247 2.710588 0.542118 10.24-6.023529 13.492706-13.43247 2.650353-9.155765 5.360941-14.034824 1.084235-15.661177-4.879059-1.566118-3.794824 8.071529-8.613647 15.661177z m123.120941-291.538824c-6.445176-1.626353-5.360941 8.011294-2.108235 6.987294 2.168471 0 4.879059 3.252706 3.734588 8.07153-1.084235 6.445176-0.542118 10.842353 4.336941 10.842353 0.542118 0 1.566118 0 1.566118-1.626353 2.228706-13.552941-4.276706-23.190588-7.529412-24.274824z m14.576941 49.453177c-5.360941 0.542118-4.336941-11.866353 12.890353-10.782118-10.782118 1.084235-6.987294 10.782118-12.890353 10.782118z m44.092236-9.155765c15.600941-6.927059 20.961882 3.794824 15.600941 5.963294-5.421176 1.566118-5.963294-8.673882-15.600941-5.963294z m65.054117-43.550118c-6.987294 0.602353-4.818824 3.734588-1.566117 4.818824 4.276706 1.204706 8.613647 8.673882 9.697882 16.685176 0 1.084235 5.360941-1.084235 5.360941-2.710588 0.481882-12.830118-10.782118-19.275294-13.492706-18.793412z m31.201883-116.133647c-4.276706-4.336941-8.613647-8.131765-12.890353-8.131764-10.782118 1.084235-5.421176 12.348235-6.987294 17.769411-2.168471 5.903059-10.179765 10.782118-4.818824 15.058824 4.879059 3.734588 8.071529-5.903059 18.31153-9.637647 2.650353-1.144471 15.058824 0.481882 17.709176-5.421177 0.481882-2.710588-6.445176-5.903059-11.324235-9.637647z m59.693176 237.628236c-10.179765-6.384941-12.348235-17.167059-16.082823-13.432471-11.324235 12.348235 13.974588 38.189176 24.69647 40.417882 6.445176 1.084235 11.324235-7.589647 9.697883-15.119058-2.168471-10.179765-9.697882-6.445176-18.31153-11.866353z" p-id="4327"></path></svg>',
  1918. mac: '<svg t="1731999754869" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7764" width="32" height="32"><path d="M849.124134 704.896288c-1.040702 3.157923-17.300015 59.872622-57.250912 118.190843-34.577516 50.305733-70.331835 101.018741-126.801964 101.909018-55.532781 0.976234-73.303516-33.134655-136.707568-33.134655-63.323211 0-83.23061 32.244378-135.712915 34.110889-54.254671 2.220574-96.003518-54.951543-130.712017-105.011682-70.934562-102.549607-125.552507-290.600541-52.30118-416.625816 36.040844-63.055105 100.821243-103.135962 171.364903-104.230899 53.160757-1.004887 103.739712 36.012192 136.028093 36.012192 33.171494 0 94.357018-44.791136 158.90615-38.089503 27.02654 1.151219 102.622262 11.298324 151.328567 81.891102-3.832282 2.607384-90.452081 53.724599-89.487104 157.76107C739.079832 663.275355 847.952448 704.467523 849.124134 704.896288M633.69669 230.749408c29.107945-35.506678 48.235584-84.314291 43.202964-132.785236-41.560558 1.630127-92.196819 27.600615-122.291231 62.896492-26.609031 30.794353-50.062186 80.362282-43.521213 128.270409C557.264926 291.935955 604.745311 264.949324 633.69669 230.749408" p-id="7765"></path></svg>'
  1919. };
  1920.  
  1921. const policy = window.trustedTypes.createPolicy('default', {
  1922. createHTML: (string, sink) => string
  1923. })
  1924.  
  1925. var defaultConfig;
  1926. var tabCount = 0;
  1927.  
  1928. function translatePage(language) {
  1929. const trans = translations[language];
  1930. document.querySelectorAll('[data-translate]').forEach(el => {
  1931. el.textContent = trans[el.getAttribute('data-translate')] || el.textContent;
  1932. });
  1933. }
  1934.  
  1935. function createTab(tabId, tabName = \`Player \${tabCount}\`, config = {}) {
  1936. const tabButton = document.createElement('button');
  1937. tabButton.className = 'tab-button';
  1938. tabButton.textContent = tabName;
  1939. tabButton.dataset.tab = tabId;
  1940. sidebar.insertBefore(tabButton, document.getElementById('global-button').nextSibling);
  1941.  
  1942. const tab = document.createElement('div');
  1943. tab.id = tabId;
  1944. tab.name = tabName;
  1945. tab.className = 'tab';
  1946. tab.setAttribute('readonly', config.readonly === true)
  1947. const disabled = config.readonly === true ? 'disabled' : '';
  1948. config.presetEvent = config.presetEvent || {
  1949. pauseAuto: true
  1950. };
  1951. tab.innerHTML = policy.createHTML(\`
  1952. <div class="header">
  1953. <button class="remove-button" data-translate="delete" \${disabled}>删除</button>
  1954. </div>
  1955. <div class="input-group">
  1956. <label data-translate="name">名称</label>
  1957. <input type="text" value="\${config.name || tabName}" name="name" placeholder="\${tabName}" required \${disabled}>
  1958. </div>
  1959. <div class="input-group">
  1960. <label data-translate="system">系统</label>
  1961. <div class="radio-button-group" name="system">
  1962. <div class="radio-button active \${disabled}" value="windows">\${SYSTEM_SVG.windows} Windows</div>
  1963. <div class="radio-button \${disabled}" value="linux">\${SYSTEM_SVG.linux} Linux</div>
  1964. <div class="radio-button \${disabled}" value="mac">\${SYSTEM_SVG.mac} Mac</div>
  1965. </div>
  1966. </div>
  1967. <div class="input-group">
  1968. <label data-translate="iconSize">图标大小</label>
  1969. <input type="number" value="\${config.iconSize || 50}" name="iconSize" min="1" required>
  1970. </div>
  1971. <div class="input-group">
  1972. <label data-translate="icon">图标</label>
  1973. <input type="search" value="\${config.icon || ''}" list="icon-list" name="icon" required>
  1974. <datalist id="icon-list">
  1975. <option value="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGU4DoFouNX7OYb16a7XBAx_dXV3OgzcXc7A&s">MPV</option>
  1976. <option value="https://images.dwncdn.net/images/t_app-icon-l/p/ab54d4c6-a4d2-11e6-99a6-00163ec9f5fa/1236096272/potplayer-32bit-logo">PotPlayer</option>
  1977. <option value="https://upload.wikimedia.org/wikipedia/commons/5/51/IINA_Logo.png">IINA</option>
  1978. </datalist>
  1979. </div>
  1980. <div class="input-group">
  1981. <label data-translate="presetEvent">预设事件</label>
  1982. <div class="checkbox-group">
  1983. <input type="checkbox" id="\${tabId}-play-auto" name="playAuto" \${config.presetEvent.playAuto ? 'checked' : ''}/>
  1984. <label for="\${tabId}-play-auto" data-translate="playAuto" class="chekbox-label">自动播放</label>
  1985. <input type="checkbox" id="\${tabId}-pause-auto" name="pauseAuto" \${config.presetEvent.pauseAuto ? 'checked' : ''}/>
  1986. <label for="\${tabId}-pause-auto" data-translate="pauseAuto" class="chekbox-label">自动暂停</label>
  1987. <input type="checkbox" id="\${tabId}-close-auto" name="closeAuto" \${config.presetEvent.closeAuto ? 'checked' : ''}/>
  1988. <label for="\${tabId}-close-auto" data-translate="closeAuto" class="chekbox-label">自动关闭</label>
  1989. </div>
  1990. </div>
  1991. <div class="input-group">
  1992. <label data-translate="playEvent">播放事件</label>
  1993. <textarea class="tab-textarea" wrap="off" \${disabled} name="playEvent">\${config.playEvent || ''}</textarea>
  1994. </div>
  1995. <div class="input-group">
  1996. <label data-translate="enable">启用</label>
  1997. <label class="switch">
  1998. <input type="checkbox" class="tab-switch" \${config.enable || config.enable === undefined ? 'checked' : ''} name="enable"><span class="switch-slider"></span>
  1999. </label>
  2000. </div>
  2001. \`);
  2002. content.appendChild(tab);
  2003.  
  2004. tab.querySelector('.remove-button').onclick = () => {
  2005. const previousElement = tabButton.previousElementSibling;
  2006. sidebar.removeChild(tabButton);
  2007. content.removeChild(tab);
  2008. activateTab(previousElement.getAttribute('data-tab'));
  2009. };
  2010.  
  2011. const nameInput = tab.querySelector('[name="name"]');
  2012. nameInput.oninput = () => {
  2013. tabButton.innerHTML = SYSTEM_SVG[tab.querySelector('[name=system] .active').getAttribute('value')] + (
  2014. nameInput.value || tabName);
  2015. };
  2016.  
  2017. config.system = config.system || 'windows';
  2018. tab.querySelectorAll('[name=system]').forEach(radioButtonGroup => {
  2019. const radioButtons = radioButtonGroup.querySelectorAll('.radio-button');
  2020. radioButtons.forEach(radioButton => {
  2021. radioButton.onclick = () => {
  2022. radioButtons.forEach(btn => btn.classList.remove('active'));
  2023. radioButton.classList.add('active');
  2024. tabButton.innerHTML = SYSTEM_SVG[radioButton.getAttribute('value')] + (nameInput
  2025. .value || tabName);
  2026. };
  2027. if (radioButton.getAttribute('value') === config.system) {
  2028. radioButton.classList.add('active');
  2029. tabButton.innerHTML = SYSTEM_SVG[radioButton.getAttribute('value')] + (nameInput
  2030. .value || tabName);
  2031. } else {
  2032. radioButton.classList.remove('active');
  2033. }
  2034. });
  2035. })
  2036.  
  2037. tabButton.onclick = () => activateTab(tabId);
  2038.  
  2039. activateTab(tabId);
  2040. }
  2041.  
  2042. function activateTab(tabId) {
  2043. document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
  2044. document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
  2045. document.getElementById(tabId).classList.add('active');
  2046. document.querySelector(\`[data-tab="\${tabId}"]\`).classList.add('active');
  2047. document.querySelector('#content').scrollTop = 0;
  2048. }
  2049.  
  2050. function saveConfig() {
  2051. const ytdlp = document.querySelector('#ytdlp');
  2052. const ytdlpRegex = ytdlp.querySelector('[name="regex"]:not([disabled])').value;
  2053.  
  2054. const bilibili = document.querySelector('#bilibili');
  2055. const bilibiliRegex = bilibili.querySelector('[name="regex"]:not([disabled])').value;
  2056.  
  2057. const config = {
  2058. global: {
  2059. parser: {}
  2060. },
  2061. players: []
  2062. };
  2063.  
  2064. for (const id in defaultConfig.global.parser) {
  2065. const parser = document.getElementById(id);
  2066. const regex = parser.querySelector('[name="regex"]:not([disabled])').value;
  2067. config.global.parser[id] = {};
  2068. config.global.parser[id].regex = regex ? regex.split('\\n') : [];
  2069. for (const name in defaultConfig.global.parser[id]) {
  2070. if (name === 'regex') {
  2071. continue;
  2072. }
  2073. config.global.parser[id][name] = parser.querySelector(\`[name=\${name}] .active\`).getAttribute('value');
  2074. }
  2075. }
  2076.  
  2077. for (const key in defaultConfig.global) {
  2078. if (key === 'parser') {
  2079. continue;
  2080. }
  2081. config.global[key] = document.getElementById(key)?.value || defaultConfig.global[key];
  2082. }
  2083.  
  2084. document.querySelectorAll('.tab').forEach(tab => {
  2085. if (tab.id !== 'global') {
  2086. config.players.push({
  2087. readonly: tab.getAttribute('readonly') === "true",
  2088. name: tab.querySelector('[name="name"]').value || tab.name || 'Player',
  2089. system: tab.querySelector('[name="system"] .active').getAttribute('value') ||
  2090. 'windows',
  2091. icon: tab.querySelector('[name="icon"]').value || '',
  2092. iconSize: tab.querySelector('[name="iconSize"]').value || 50,
  2093. playEvent: tab.querySelector('[name="playEvent"]').value || '',
  2094. presetEvent: {
  2095. playAuto: tab.querySelector('[name="playAuto"]').checked,
  2096. pauseAuto: tab.querySelector('[name="pauseAuto"]').checked,
  2097. closeAuto: tab.querySelector('[name="closeAuto"]').checked,
  2098. },
  2099. enable: tab.querySelector('[name="enable"]').checked,
  2100. });
  2101. }
  2102. });
  2103.  
  2104. if (window.self === window.top) {
  2105. localStorage.setItem('config', JSON.stringify(config));
  2106. } else {
  2107. parent.postMessage(config, '*');
  2108. }
  2109. };
  2110.  
  2111. function resetButtonCoord() {
  2112. document.getElementById('buttonXCoord').value = defaultConfig.global.buttonXCoord;
  2113. document.getElementById('buttonYCoord').value = defaultConfig.global.buttonYCoord;
  2114. }
  2115.  
  2116. function loadConfig(config) {
  2117. // 全局配置
  2118. for (const key in config.global) {
  2119. if (key === 'parser' || !document.getElementById(key)) {
  2120. continue;
  2121. }
  2122. document.getElementById(key).value = config.global[key];
  2123. }
  2124.  
  2125. document.getElementById('language').value = config.global.language;
  2126. language.dispatchEvent(new Event("change"));
  2127.  
  2128. document.querySelectorAll('.parser').forEach(parser => {
  2129. parser.querySelectorAll('.radio-button-group').forEach(radioButtonGroup => {
  2130. const radioButtons = radioButtonGroup.querySelectorAll('.radio-button');
  2131. radioButtons.forEach(radioButton => {
  2132. if (radioButton.getAttribute('value') === config.global.parser[parser.id][
  2133. radioButtonGroup.getAttribute('name')
  2134. ]) {
  2135. radioButton.classList.add('active');
  2136. } else {
  2137. radioButton.classList.remove('active');
  2138. }
  2139. });
  2140. })
  2141. parser.querySelectorAll('textarea').forEach(textarea => {
  2142. if (textarea.disabled) {
  2143. const regex = defaultConfig.global.parser[parser.id][textarea.getAttribute(
  2144. 'name')] || [];
  2145. if (regex.length > 0) {
  2146. textarea.value = regex.join('\\n');
  2147. textarea.style.height = regex.length * 20 + 20 + 'px';
  2148. } else {
  2149. textarea.style.display = 'none';
  2150. }
  2151. } else {
  2152. const regex = config.global.parser[parser.id][textarea.getAttribute('name')] || [];
  2153. textarea.value = regex.join('\\n');
  2154. }
  2155. })
  2156. })
  2157.  
  2158. // 播放器配置
  2159. removeAllTab();
  2160. config.players.forEach(player => createTab(\`player\${tabCount++}\`, player.name, player));
  2161.  
  2162. // 默认选中全局配置
  2163. activateTab('global');
  2164. }
  2165.  
  2166. function removeAllTab() {
  2167. document.querySelectorAll('.tab-button').forEach(tabButton => {
  2168. if (tabButton.id === 'global-button') {
  2169. return;
  2170. }
  2171. sidebar.removeChild(tabButton);
  2172. })
  2173. document.querySelectorAll('.tab').forEach(tab => {
  2174. if (tab.id === 'global') {
  2175. return;
  2176. }
  2177. content.removeChild(tab);
  2178. })
  2179. }
  2180.  
  2181. function resetConfig() {
  2182. let config = JSON.parse(JSON.stringify(defaultConfig));
  2183. for (const key in config.global.parser) {
  2184. config.global.parser[key].regex = [];
  2185. }
  2186. loadConfig(config);
  2187. }
  2188.  
  2189. function init() {
  2190. if (window.self === window.top) {
  2191. return;
  2192. }
  2193.  
  2194. window.addEventListener('message', function (event) {
  2195. if (event.data.defaultConfig && event.data.config) {
  2196. defaultConfig = event.data.defaultConfig;
  2197. loadConfig(event.data.config);
  2198. document.getElementById('sidebar-container').style.display = 'flex';
  2199. document.getElementById('content-container').style.display = 'flex';
  2200. }
  2201. });
  2202.  
  2203. document.getElementById('language').addEventListener('change', (e) => {
  2204. translatePage(e.target.value);
  2205. });
  2206. document.getElementById('add-tab-button').onclick = () => createTab(\`tab\${tabCount++}\`);
  2207. document.getElementById('global-button').onclick = () => activateTab('global');
  2208. document.getElementById('save-button').onclick = () => saveConfig();
  2209. document.getElementById('reset-button').onclick = () => resetConfig();
  2210. document.getElementById('reset-button-coord-button').onclick = () => resetButtonCoord();
  2211.  
  2212. document.querySelectorAll('#global .radio-button-group').forEach(radioButtonGroup => {
  2213. const radioButtons = radioButtonGroup.querySelectorAll('.radio-button');
  2214. radioButtons.forEach(radioButton => {
  2215. radioButton.onclick = () => {
  2216. radioButtons.forEach(btn => btn.classList.remove('active'));
  2217. radioButton.classList.add('active');
  2218. };
  2219. });
  2220. })
  2221. }
  2222.  
  2223. init();
  2224. </script>
  2225.  
  2226. </html>
  2227. `;
  2228. if (SETTING_URL) {
  2229. const response = await fetch(SETTING_URL);
  2230. settingIframeHtml = await response.text();
  2231. }
  2232. settingIframe.onload = function () {
  2233. const doc = settingIframe.contentDocument || settingIframe.contentWindow.document;
  2234. doc.open();
  2235. doc.write(policy.createHTML(settingIframeHtml));
  2236. doc.close();
  2237. };
  2238. document.body.appendChild(settingIframe);
  2239.  
  2240. window.addEventListener('message', function (event) {
  2241. if (event.data && event.data.global) {
  2242. // 保存配置
  2243. currentConfig = event.data;
  2244. GM_setValue('config', currentConfig);
  2245. showToast(translation.saveSuccessfully);
  2246.  
  2247. // 移除旧元素
  2248. document.head.removeChild(style);
  2249. document.body.removeChild(buttonDiv);
  2250. style = undefined;
  2251. buttonDiv = undefined;
  2252.  
  2253. // 重新初始化
  2254. isReloading = true;
  2255. init(currentUrl);
  2256. }
  2257. });
  2258.  
  2259. try {
  2260. showLoading();
  2261. await sleep(REFRESH_INTERVAL);
  2262. } finally {
  2263. hideLoading();
  2264. }
  2265. }
  2266.  
  2267. function startFlashing(element) {
  2268. let visibility = element.style.visibility;
  2269. let transition = element.style.transition;
  2270. let boxShadow = element.style.boxShadow;
  2271.  
  2272. element.style.visibility = 'visible';
  2273. element.style.transition = 'box-shadow 0.5s ease';
  2274. let isGlowing = false;
  2275. const interval = setInterval(() => {
  2276. isGlowing = !isGlowing;
  2277. element.style.boxShadow = isGlowing ? `0 0 10px 10px ${COLOR.PRIMARY}` : 'none';
  2278. }, 500);
  2279.  
  2280. setTimeout(() => {
  2281. clearInterval(interval);
  2282. element.style.visibility = visibility;
  2283. element.transition = transition;
  2284. element.boxShadow = boxShadow;
  2285. }, 5000);
  2286. }
  2287.  
  2288. // ======================================== 开始执行 =======================================
  2289.  
  2290. function init(url) {
  2291. currentConfig = loadConfig();
  2292. translation = translations[currentConfig.global.language];
  2293. appendCss();
  2294. appendToastDiv();
  2295. appendLoadingDiv();
  2296. appendButtonDiv();
  2297. currentParser = matchParser(currentConfig.global.parser, url) || matchParser(defaultConfig.global.parser, url);
  2298. if (currentParser) {
  2299. buttonDiv.style.display = 'flex';
  2300. if (!isReloading) {
  2301. for (const player of currentConfig.players) {
  2302. if (player.presetEvent.playAuto === true) {
  2303. currentParser.play(player);
  2304. }
  2305. }
  2306. }
  2307. isReloading = false;
  2308. }
  2309. currentUrl = url;
  2310. }
  2311.  
  2312. onload = () => {
  2313. setInterval(() => {
  2314. const url = location.href;
  2315. if (currentUrl !== url || !buttonDiv) {
  2316. console.log(`current url update: ${currentUrl ? currentUrl + ' => ' : ''}${url}`);
  2317. init(url);
  2318. }
  2319. }, REFRESH_INTERVAL);
  2320.  
  2321. // 快捷键
  2322. document.addEventListener('keydown', (event) => {
  2323. // 打开设置:Ctrl + Alt + E
  2324. if (event.ctrlKey && event.altKey && (event.key === 'e' || event.key === 'E')) {
  2325. event.preventDefault();
  2326. startFlashing(settingButton);
  2327. settingButton.click();
  2328. }
  2329. });
  2330. };