// ==UserScript== // @name Jellyfin danmaku extension // @description Jellyfin弹幕插件 // @namespace https://github.com/RyoLee // @author RyoLee // @version 1.12 // @copyright 2022, RyoLee (https://github.com/RyoLee) // @license MIT; https://raw.githubusercontent.com/killgfat/jellyfin-danmaku/jellyfin/LICENSE // @icon https://github.githubassets.com/pinned-octocat.svg // @grant GM_xmlhttpRequest // @connect * // @match *://*/web/index.html // ==/UserScript== const fontWeight = 600 // 修改此处设置弹幕字重 const fontSize = 24; // 修改此处设置弹幕字体大小 const opacity = 0.7; //修改此处设置弹幕透明度 (async function () { 'use strict'; if (document.querySelector('meta[name="application-name"]').content == 'Jellyfin') { // ------ configs start------ let deviceId = localStorage.getItem('_deviceId2'); const serversInfo = JSON.parse(localStorage.getItem('jellyfin_credentials')).Servers; let authorization = ''; let userId = ''; let isInTampermonkey = true; let apiPrefix = 'https://api.9-ch.com/cors/'; const debugInfoLoc = 'ui'; // 'console' or 'ui' let logQueue = []; let logLines = 0; const baseUrl = window.location.origin + window.location.pathname.replace('/web/index.html', ''); const check_interval = 200; const chConverTtitle = ['当前状态: 未启用', '当前状态: 转换为简体', '当前状态: 转换为繁体']; // 0:当前状态关闭 1:当前状态打开 const danmaku_icons = ['comment', 'comments_disabled']; const search_icon = 'find_replace'; const translate_icon = 'g_translate'; const info_icon = 'import_contacts'; const filter_icons = ['filter_none', 'filter_1', 'filter_2', 'filter_3']; const source_icon = 'library_add'; const log_icon = 'adb'; const spanClass = 'xlargePaperIconButton material-icons '; const buttonOptions = { class: 'paper-icon-button-light', is: 'paper-icon-button-light', }; const uiAnchorStr = 'pause'; const uiQueryStr = '.osdTimeText'; const mediaContainerQueryStr = "div[data-type='video-osd']"; // const mediaContainerQueryStr = "div[class='videoPlayerContainer']"; const mediaQueryStr = 'video'; const displayButtonOpts = { title: '弹幕开关', id: 'displayDanmaku', //innerText: null, class: '', onclick: () => { if (window.ede.loading) { showDebugInfo('正在加载,请稍后再试'); return; } showDebugInfo('切换弹幕开关'); window.ede.danmakuSwitch = (window.ede.danmakuSwitch + 1) % 2; window.localStorage.setItem('danmakuSwitch', window.ede.danmakuSwitch); document.querySelector('#displayDanmaku').children[0].className = spanClass + danmaku_icons[window.ede.danmakuSwitch]; if (window.ede.danmaku) { window.ede.danmakuSwitch == 1 ? window.ede.danmaku.show() : window.ede.danmaku.hide(); } }, }; const searchButtonOpts = { title: '搜索弹幕', id: 'searchDanmaku', //innerText: search_icon, class: search_icon, onclick: () => { if (window.ede.loading) { showDebugInfo('正在加载,请稍后再试'); return; } showDebugInfo('手动匹配弹幕'); reloadDanmaku('search'); }, }; const translateButtonOpts = { title: null, id: 'translateDanmaku', //innerText: translate_icon, class: translate_icon, onclick: () => { if (window.ede.loading) { showDebugInfo('正在加载,请稍后再试'); return; } showDebugInfo('切换简繁转换'); window.ede.chConvert = (window.ede.chConvert + 1) % 3; window.localStorage.setItem('chConvert', window.ede.chConvert); document.querySelector('#translateDanmaku').setAttribute('title', chConverTtitle[window.ede.chConvert]); reloadDanmaku('reload'); showDebugInfo(document.querySelector('#translateDanmaku').getAttribute('title')); }, }; const infoButtonOpts = { title: '弹幕信息', id: 'printDanmakuInfo', //innerText: info_icon, class: info_icon, onclick: () => { if (!window.ede.episode_info || window.ede.loading) { showDebugInfo('正在加载,请稍后再试'); return; } showDebugInfo('显示当前信息'); let msg = '动画名称:' + window.ede.episode_info.animeTitle; if (window.ede.episode_info.episodeTitle) { msg += '\n分集名称:' + window.ede.episode_info.episodeTitle; } sendNotification('当前弹幕匹配', msg); }, }; const filterButtonOpts = { title: '过滤等级(下次加载生效)', id: 'filteringDanmaku', //innerText: null, class: '', onclick: () => { showDebugInfo('切换弹幕过滤等级'); let level = window.localStorage.getItem('danmakuFilterLevel'); level = ((level ? parseInt(level) : 0) + 1) % 4; window.localStorage.setItem('danmakuFilterLevel', level); document.querySelector('#filteringDanmaku').children[0].className = spanClass + filter_icons[level]; }, }; const sourceButtonOpts = { title: '手动增加弹幕源', id: 'addDanmakuSource', class: source_icon, onclick: () => { showDebugInfo('手动增加弹幕源'); let source = prompt('请输入弹幕源地址:'); if (source) { getCommentsByUrl(source).then((comments) => { createDanmaku(comments).then(() => { showDebugInfo('弹幕就位'); }); }); } }, } const logButtonOpts = { title: '日志开关', id: 'displayLog', class: log_icon, onclick: () => { if (window.ede.loading) { showDebugInfo('正在加载,请稍后再试'); return; } // showDebugInfo('切换日志开关'); window.ede.logSwitch = (window.ede.logSwitch + 1) % 2; window.localStorage.setItem('logSwitch', window.ede.logSwitch); let logSpan = document.querySelector('#debugInfo'); if (logSpan) { window.ede.logSwitch == 1 ? (logSpan.style.display = 'block') : (logSpan.style.display = 'none'); } }, }; // ------ configs end------ /* eslint-disable */ /* https://cdn.jsdelivr.net/npm/danmaku/dist/danmaku.min.js */ // prettier-ignore !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Danmaku=e()}(this,(function(){"use strict";var t=function(){if("undefined"==typeof document)return"transform";for(var t=["oTransform","msTransform","mozTransform","webkitTransform","transform"],e=document.createElement("div").style,i=0;i0&&a!==1/0?Math.ceil(a):1*!!o.strokeStyle,h.font=o.font,t.width=t.width||Math.max(1,Math.ceil(h.measureText(t.text).width)+2*a),t.height=t.height||Math.ceil(function(t,e){if(s[t])return s[t];var i=12,n=t.match(/(\d+(?:\.\d+)?)(px|%|em|rem)(?:\s*\/\s*(\d+(?:\.\d+)?)(px|%|em|rem)?)?/);if(n){var r=1*n[1]||10,h=n[2],o=1*n[3]||1.2,a=n[4];"%"===h&&(r*=e.container/100),"em"===h&&(r*=e.container),"rem"===h&&(r*=e.root),"px"===a&&(i=o),"%"===a&&(i=r*o/100),"em"===a&&(i=r*o),"rem"===a&&(i=e.root*o),void 0===a&&(i=r*o)}return s[t]=i,i}(o.font,e))+2*a,r.width=t.width*n,r.height=t.height*n,h.scale(n,n),o)h[d]=o[d];var u=0;switch(o.textBaseline){case"top":case"hanging":u=a;break;case"middle":u=t.height>>1;break;default:u=t.height-a}return o.strokeStyle&&h.strokeText(t.text,a,u),h.fillText(t.text,a,u),r}function h(t){return 1*window.getComputedStyle(t,null).getPropertyValue("font-size").match(/(.+)px/)[1]}var o={name:"canvas",init:function(t){var e=document.createElement("canvas");return e.context=e.getContext("2d"),e._fontSize={root:h(document.getElementsByTagName("html")[0]),container:h(t)},e},clear:function(t,e){t.context.clearRect(0,0,t.width,t.height);for(var i=0;ir)return!0;var h=e._.duration+t.time-i,o=e._.width+s.width,a=e.media?s.time:s._utc,d=o*(i-a)*n/e._.duration,u=e._.width-d;return h>e._.duration*u/(e._.width+s.width)}for(var r=this._.space[t.mode],h=0,o=0,a=1;a=u){o=a;break}s(d,t)&&(h=a)}var m=r[h].range,c={range:m+t.height,time:this.media?t.time:t._utc,width:t.width,height:t.height};return r.splice(h+1,o-h-1,c),"bottom"===t.mode?this._.height-t.height-m%this._.height:m%(this._.height-t.height)}var d="undefined"!=typeof window&&(window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(t){return setTimeout(t,50/3)},u="undefined"!=typeof window&&(window.cancelAnimationFrame||window.mozCancelAnimationFrame||window.webkitCancelAnimationFrame)||clearTimeout;function m(t,e,i){for(var n=0,s=0,r=t.length;s=t[n=s+r>>1][e]?s=n:r=n;return t[s]&&i=0;u--)o=this._.runningList[u],r-(d=this.media?o.time:o._utc)>this._.duration&&(n(this._.stage,o),this._.runningList.splice(u,1));for(var m=[];this._.position=r));)r-d>this._.duration||(this.media&&(o._utc=s-(this.media.currentTime-o.time)),m.push(o)),++this._.position;for(e(this._.stage,m),u=0;u>1),i(this._.stage,o)}}}(this._.engine.framing.bind(this),this._.engine.setup.bind(this),this._.engine.render.bind(this),this._.engine.remove.bind(this));return this._.requestID=d((function t(){n.call(i),i._.requestID=d(t)})),this}function g(){return!this._.visible||this._.paused||(this._.paused=!0,u(this._.requestID),this._.requestID=0),this}function _(){if(!this.media)return this;this.clear(),f(this._.space);var t=m(this.comments,"time",this.media.currentTime);return this._.position=Math.max(0,t-1),this}function v(t){t.play=p.bind(this),t.pause=g.bind(this),t.seeking=_.bind(this),this.media.addEventListener("play",t.play),this.media.addEventListener("pause",t.pause),this.media.addEventListener("playing",t.play),this.media.addEventListener("waiting",t.pause),this.media.addEventListener("seeking",t.seeking)}function w(t){this.media.removeEventListener("play",t.play),this.media.removeEventListener("pause",t.pause),this.media.removeEventListener("playing",t.play),this.media.removeEventListener("waiting",t.pause),this.media.removeEventListener("seeking",t.seeking),t.play=null,t.pause=null,t.seeking=null}function y(t){this._={},this.container=t.container||document.createElement("div"),this.media=t.media,this._.visible=!0,this.engine=(t.engine||"DOM").toLowerCase(),this._.engine="canvas"===this.engine?o:i,this._.requestID=0,this._.speed=Math.max(0,t.speed)||144,this._.duration=4,this.comments=t.comments||[],this.comments.sort((function(t,e){return t.time-e.time}));for(var e=0;e setTimeout(resolve, 200)); span = document.getElementById('debugInfo'); } if (logLines < 10) { logLines++; logQueue.push(msg); } else { logQueue.shift(); logQueue.push(msg); } span.innerText = ''; logQueue.forEach((line) => { span.innerText += line + '\n'; }); } else if (debugInfoLoc == 'console') { console.log(msg); } } function sendNotification(title, msg) { const Notification = window.Notification || window.webkitNotifications; showDebugInfo(msg); if (Notification.permission === 'granted') { return new Notification(title, { body: msg, }); } else { Notification.requestPermission((permission) => { if (permission === 'granted') { return new Notification(title, { body: msg, }); } }); } } async function initConfig() { showDebugInfo('serverInfo'); // let srvInfo = await fetch(baseUrl + '/System/Info/Public').then(res => res.json()); // let token = ''; // let serverId = srvInfo.Id; // serversInfo.forEach(data => { // if (data.Id == serverId) { // token = data.AccessToken; // userId = data.UserId; // } // }); let token = serversInfo[0].AccessToken; userId = serversInfo[0].UserId; let sessionUrl = baseUrl + '/Sessions?ControllableByUserId=' + userId if (deviceId) { sessionUrl += '&DeviceId=' + deviceId; } showDebugInfo('Get DevId'); let sessionInfo = await fetch(sessionUrl, { "credentials": "include", "headers": { "Accept": "application/json", "Authorization": "MediaBrowser Token=\"" + token + "\"" }, "method": "GET", "mode": "cors" }).then(res => res.json()); if (!deviceId) { deviceId = sessionInfo[0].DeviceId; localStorage.setItem('_deviceId2', deviceId); } let clientName = sessionInfo[0].Client; let deviceName = sessionInfo[0].DeviceName; let serverVersion = sessionInfo[0].ApplicationVersion; // Ref: https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f authorization = "MediaBrowser Client=\"" + clientName + "\", Device=\"" + deviceName + "\", DeviceId=\"" + deviceId + "\", Version=\"" + serverVersion + "\", Token=\"" + token + "\""; } async function getEmbyItemInfo() { showDebugInfo('准备获取Item信息'); if (authorization.length > 0 && userId.length > 0 && deviceId.length > 0) { showDebugInfo('正在获取Item信息'); let playingInfo = null; while (!playingInfo) { await new Promise((resolve) => setTimeout(resolve, 200)); let sessionUrl = baseUrl + '/Sessions?ControllableByUserId=' + userId + '&deviceId=' + deviceId; showDebugInfo(sessionUrl); let sessionInfo = await fetch(sessionUrl, { "credentials": "include", "headers": { "Accept": "application/json", "Authorization": authorization }, "method": "GET", "mode": "cors" }).then(res => res.json()); playingInfo = sessionInfo[0].NowPlayingItem; } showDebugInfo('成功 ' + playingInfo.SeriesName); return playingInfo; } else { showDebugInfo('等待Config'); await initConfig(); } } function makeGetRequest(url) { if (isInTampermonkey) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { "Accept-Encoding": "gzip", "Accept": "application/json" }, onload: function (response) { resolve(response.responseText); }, onerror: function (error) { reject(error); } }); }); } else { return fetch(url, { method: 'GET', headers: { "Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": navigator.userAgent } }) } } async function getEpisodeInfo(is_auto = true) { let item = await getEmbyItemInfo(); if (!item) { return null; } let _id; let animeName; let anime_id = -1; let episode; if (item.Type == 'Episode') { _id = item.SeasonId; animeName = item.SeriesName; episode = item.IndexNumber; let session = item.ParentIndexNumber; if (session != 1) { animeName += ' ' + session; } } else { _id = item.Id; animeName = item.Name; episode = 'movie'; } let _id_key = '_anime_id_rel_' + _id; let _name_key = '_anime_name_rel_' + _id; let _episode_key = '_episode_id_rel_' + _id + '_' + episode; if (is_auto) { if (window.localStorage.getItem(_episode_key)) { return JSON.parse(window.localStorage.getItem(_episode_key)); } } if (window.localStorage.getItem(_id_key)) { anime_id = window.localStorage.getItem(_id_key); } if (window.localStorage.getItem(_name_key)) { animeName = window.localStorage.getItem(_name_key); } if (!is_auto) { animeName = prompt('确认动画名:', animeName); } let searchUrl = apiPrefix + 'https://api.dandanplay.net/api/v2/search/episodes?anime=' + animeName + '&withRelated=true'; if (is_auto) { searchUrl += '&episode=' + episode; } let animaInfo = await makeGetRequest(searchUrl) .then((response) => isInTampermonkey ? JSON.parse(response) : response.json()) .catch((error) => { showDebugInfo('查询失败:', error); return null; }); if (animaInfo.animes.length == 0) { showDebugInfo('弹幕查询无结果'); //alert('弹幕查询无结果'); return null; } showDebugInfo('查询成功'); showDebugInfo(animaInfo); let selecAnime_id = 1; if (anime_id != -1) { for (let index = 0; index < animaInfo.animes.length; index++) { if (animaInfo.animes[index].animeId == anime_id) { selecAnime_id = index + 1; } } } if (!is_auto) { let anime_lists_str = list2string(animaInfo); showDebugInfo(anime_lists_str); selecAnime_id = prompt('选择:\n' + anime_lists_str, selecAnime_id); selecAnime_id = parseInt(selecAnime_id) - 1; window.localStorage.setItem(_id_key, animaInfo.animes[selecAnime_id].animeId); window.localStorage.setItem(_name_key, animaInfo.animes[selecAnime_id].animeTitle); let episode_lists_str = ep2string(animaInfo.animes[selecAnime_id].episodes); episode = prompt('确认集数:\n' + episode_lists_str, parseInt(episode)); episode = parseInt(episode) - 1; } else { selecAnime_id = parseInt(selecAnime_id) - 1; episode = 0; } let episodeInfo = { episodeId: animaInfo.animes[selecAnime_id].episodes[episode].episodeId, animeTitle: animaInfo.animes[selecAnime_id].animeTitle, episodeTitle: animaInfo.animes[selecAnime_id].type == 'tvseries' ? animaInfo.animes[selecAnime_id].episodes[episode].episodeTitle : null, }; window.localStorage.setItem(_episode_key, JSON.stringify(episodeInfo)); return episodeInfo; } function getComments(episodeId) { let url = apiPrefix + 'https://api.dandanplay.net/api/v2/comment/' + episodeId + '?withRelated=true&chConvert=' + window.ede.chConvert; return makeGetRequest(url) .then((response) => isInTampermonkey ? JSON.parse(response) : response.json()) .then((data) => { showDebugInfo('弹幕下载成功: ' + data.comments.length); return data.comments; }) .catch((error) => { showDebugInfo('获取弹幕失败:', error); return null; }); } async function getCommentsByUrl(src) { const url_encoded = encodeURIComponent(src); const url = apiPrefix + 'https://api.dandanplay.net/api/v2/extcomment?url=' + url_encoded; let comments = []; for (let i = 0; i < 2; i++) { comments = makeGetRequest(url) .then((response) => isInTampermonkey ? JSON.parse(response) : response.json()) .then((data) => { showDebugInfo('弹幕下载成功: ' + data.comments.length); return data.comments; }) .catch((error) => { showDebugInfo('获取弹幕失败:', error); return null; }); if (comments.length > 0) { break; } await new Promise((resolve) => setTimeout(resolve, 3000)); } return comments; } async function createDanmaku(comments) { if (!comments) { return; } if (window.ede.danmaku != null) { window.ede.danmaku.clear(); window.ede.danmaku.destroy(); window.ede.danmaku = null; } let _comments = danmakuFilter(danmakuParser(comments)); showDebugInfo('弹幕加载成功: ' + _comments.length); while (!document.querySelector(mediaContainerQueryStr)) { await new Promise((resolve) => setTimeout(resolve, 200)); } let _container = null; document.querySelectorAll(mediaContainerQueryStr).forEach(function (element) { if (!element.classList.contains('hide')) { _container = element; } }); if (!_container) { showDebugInfo('未找到播放器'); } let _media = document.querySelector(mediaQueryStr); if (!_media) { showDebugInfo('未找到video'); } else { //prompt(_media.outerHTML); } // showDebugInfo(_comments[0].text) showDebugInfo(_container.id + ' ' + _media.className) window.ede.danmaku = new Danmaku({ container: _container, media: _media, comments: _comments, engine: 'canvas', }); _container.childNodes.forEach(function (element) { if (element.nodeName == 'CANVAS') { element.style.position = 'absolute'; element.style.top = '0px'; element.style.bottom = '0px'; element.style.opacity = `${opacity}`; } }); // window.ede.danmaku.emit({ // text: 'example', // mode: 'rtl', // time: 5.0, // }); window.ede.danmakuSwitch == 1 ? window.ede.danmaku.show() : window.ede.danmaku.hide(); if (window.ede.ob) { window.ede.ob.disconnect(); } window.ede.ob = new ResizeObserver(() => { if (window.ede.danmaku) { showDebugInfo('Resizing'); window.ede.danmaku.resize(); } }); window.ede.ob.observe(_container); } function reloadDanmaku(type = 'check') { if (window.ede.loading) { showDebugInfo('正在重新加载'); return; } window.ede.loading = true; getEpisodeInfo(type != 'search') .then((info) => { return new Promise((resolve, reject) => { if (!info) { if (type != 'init') { reject('播放器未完成加载'); } else { reject(null); } } if (type != 'search' && type != 'reload' && window.ede.danmaku && window.ede.episode_info && window.ede.episode_info.episodeId == info.episodeId) { reject('当前播放视频未变动'); } else { window.ede.episode_info = info; resolve(info.episodeId); } }); }) .then( (episodeId) => getComments(episodeId).then((comments) => createDanmaku(comments).then(() => { showDebugInfo('弹幕就位'); }), ), (msg) => { if (msg) { showDebugInfo(msg); } }, ) .then(() => { window.ede.loading = false; if (document.getElementById('danmakuCtr').style.opacity != 1) { document.getElementById('danmakuCtr').style.opacity = 1; } }); } function danmakuFilter(comments) { let level = parseInt(window.localStorage.getItem('danmakuFilterLevel') ? window.localStorage.getItem('danmakuFilterLevel') : 0); if (level == 0) { return comments; } let limit = 9 - level * 2; let vertical_limit = 6; let arr_comments = []; let vertical_comments = []; for (let index = 0; index < comments.length; index++) { let element = comments[index]; let i = Math.ceil(element.time); let i_v = Math.ceil(element.time / 3); if (!arr_comments[i]) { arr_comments[i] = []; } if (!vertical_comments[i_v]) { vertical_comments[i_v] = []; } // TODO: 屏蔽过滤 if (vertical_comments[i_v].length < vertical_limit) { vertical_comments[i_v].push(element); } else { element.mode = 'rtl'; } if (arr_comments[i].length < limit) { arr_comments[i].push(element); } } return arr_comments.flat(); } function danmakuParser($obj) { //const $xml = new DOMParser().parseFromString(string, 'text/xml') // const fontSize = Math.round(((window.screen.height > window.screen.width ? window.screen.width : window.screen.height) / 1080) * 24); showDebugInfo('Screen: ' + window.screen.width + 'x' + window.screen.height); showDebugInfo('fontSize: ' + fontSize); return $obj .map(($comment) => { const p = $comment.p; //if (p === null || $comment.childNodes[0] === undefined) return null const values = p.split(','); const mode = { 6: 'ltr', 1: 'rtl', 5: 'top', 4: 'bottom' }[values[1]]; if (!mode) return null; // const fontSize = Number(values[2]) || 25 // const fontSize = Math.round((window.screen.height > window.screen.width ? window.screen.width : window.screen.height / 1080) * 18); const color = `000000${Number(values[2]).toString(16)}`.slice(-6); return { text: $comment.m, mode, time: values[0] * 1, style: { // For DOMRenderer: // fontSize: `${fontSize}px`, // color: `#${color}`, // textShadow: // color === '00000' ? '-1px -1px #fff, -1px 1px #fff, 1px -1px #fff, 1px 1px #fff' : '-1px -1px #000, -1px 1px #000, 1px -1px #000, 1px 1px #000', // For CanvasRenderer: font: `${fontWeight} ${fontSize}px YaHei,sans-serif`, fillStyle: `#${color}`, strokeStyle: color === '000000' ? '#fff' : '#000', lineWidth: 2.0, shadowOffsetX: -3,//用来设定阴影在 X轴的延伸距 shadowOffsetX: -3,//用来设定阴影在 Y轴的延伸距 shadowBlur: 2,//设定阴影的模糊程度 默认0 shadowColor: "rgba(0, 0, 0, 0.5)",//设定阴影颜色效果 // globalAlpha = 0.5, }, }; }) .filter((x) => x); } function list2string($obj2) { const $animes = $obj2.animes; let anime_lists = $animes.map(($single_anime) => { return $single_anime.animeTitle + ' 类型:' + $single_anime.typeDescription; }); let anime_lists_str = '1:' + anime_lists[0]; for (let i = 1; i < anime_lists.length; i++) { anime_lists_str = anime_lists_str + '\n' + (i + 1).toString() + ':' + anime_lists[i]; } return anime_lists_str; } function ep2string($obj3) { const $animes = $obj3; let anime_lists = $animes.map(($single_ep) => { return $single_ep.episodeTitle; }); let ep_lists_str = '1:' + anime_lists[0]; for (let i = 1; i < anime_lists.length; i++) { ep_lists_str = ep_lists_str + '\n' + (i + 1).toString() + ':' + anime_lists[i]; } return ep_lists_str; } while (!document.querySelector('.htmlvideoplayer')) { await new Promise((resolve) => setTimeout(resolve, 200)); } if (!window.ede) { window.ede = new EDE(); setInterval(() => { initUI(); }, check_interval); while (!(await getEmbyItemInfo())) { await new Promise((resolve) => setTimeout(resolve, 200)); } reloadDanmaku('init'); setInterval(() => { initListener(); }, check_interval); } } })();