// ==UserScript== // @name Jellyfin danmaku extension // @description Jellyfin弹幕插件 // @namespace https://github.com/RyoLee // @author RyoLee // @version 1.52 // @copyright 2022, RyoLee (https://github.com/RyoLee) // @license MIT; https://raw.githubusercontent.com/Izumiko/jellyfin-danmaku/jellyfin/LICENSE // @icon https://github.githubassets.com/pinned-octocat.svg // @updateURL https://cdn.jsdelivr.net/gh/Izumiko/jellyfin-danmaku@gh-pages/ede.user.js // @downloadURL https://cdn.jsdelivr.net/gh/Izumiko/jellyfin-danmaku@gh-pages/ede.user.js // @connect * // @match *://*/*/web/index.html // @match *://*/web/index.html // @match *://*/*/web/ // @match *://*/web/ // @match https://jellyfin-web.pages.dev/ // ==/UserScript== (async function () { 'use strict'; if (document.querySelector('meta[name="application-name"]').content !== 'Jellyfin') { return; } // ------ configs start------ const corsProxy = 'https://ddplay-api.930524.xyz/cors/'; const apiPrefix = corsProxy + 'https://api.dandanplay.net'; const authPrefix = corsProxy + 'https://api.dandanplay.net'; // 在Worker上计算Hash let ddplayStatus = JSON.parse(localStorage.getItem('ddplayStatus')) || { isLogin: false, token: '', tokenExpire: 0 }; const check_interval = 200; // 0:当前状态关闭 1:当前状态打开 let danmaku_icons = ['comments_disabled', 'comment']; const search_icon = 'find_replace'; const source_icon = 'library_add'; let log_icons = ['code_off', 'code']; const settings_icon = 'tune'; const send_icon = 'send'; const spanClass = 'xlargePaperIconButton material-icons '; const buttonOptions = { class: 'paper-icon-button-light', is: 'paper-icon-button-light', }; const uiAnchorStr = 'pause'; const uiQueryStr = '.btnPause'; const mediaContainerQueryStr = "div[data-type='video-osd']"; const mediaQueryStr = 'video'; let isNewJellyfin = true; let itemId = ''; // Intercept XMLHttpRequest const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (_, url) { this.addEventListener('load', function () { if (url.endsWith('PlaybackInfo')) { const res = JSON.parse(this.responseText); itemId = res.MediaSources[0].Id; } }); originalOpen.apply(this, arguments); }; const displayButtonOpts = { title: '弹幕开关', id: 'displayDanmaku', 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', class: search_icon, onclick: () => { if (window.ede.loading) { showDebugInfo('正在加载,请稍后再试'); return; } showDebugInfo('手动匹配弹幕'); reloadDanmaku('search'); }, }; const sourceButtonOpts = { title: '增加弹幕源', id: 'addDanmakuSource', class: source_icon, onclick: () => { showDebugInfo('手动增加弹幕源'); let source = prompt('请输入弹幕源地址:'); if (source) { getCommentsByUrl(source) .then(comments => { if (comments !== null) { createDanmaku(comments) .then(() => { showDebugInfo('弹幕就位'); // 如果已经登录,把弹幕源提交给弹弹Play if (ddplayStatus.isLogin) { postRelatedSource(source); } }) .catch(error => { console.error('创建弹幕失败:', error); }); } } ) } else { showDebugInfo('未获取弹幕源地址'); } }, }; const logButtonOpts = { title: '日志开关', id: 'displayLog', onclick: () => { if (window.ede.loading) { showDebugInfo('正在加载,请稍后再试'); return; } window.ede.logSwitch = (window.ede.logSwitch + 1) % 2; window.localStorage.setItem('logSwitch', window.ede.logSwitch); document.querySelector('#displayLog').children[0].className = spanClass + log_icons[window.ede.logSwitch]; let logSpan = document.querySelector('#debugInfo'); if (logSpan) { window.ede.logSwitch == 1 ? (logSpan.style.display = 'block') && showDebugInfo('开启日志显示') : (logSpan.style.display = 'none'); } } }; const settingButtonOpts = { title: '弹幕设置', id: 'danmakuSettings', class: settings_icon, onclick: () => { if (document.getElementById('danmakuModal')) { return; } const modal = document.createElement('div'); modal.id = 'danmakuModal'; modal.className = 'dialogContainer'; modal.innerHTML = `
透明度:
弹幕速度:
字体大小:
高度比例:
密度限制等级:
`; document.body.appendChild(modal); function showCurrentVal(id, ticks) { const val = document.getElementById(id).value; const span = document.getElementById('lb' + id); const prefix = span.innerText.split(':')[0]; if (ticks) { span.innerText = prefix + ': ' + ticks[val]; } else { span.innerText = prefix + ': ' + val; } } showCurrentVal('opacity'); showCurrentVal('speed'); showCurrentVal('fontSize'); showCurrentVal('heightRatio'); showCurrentVal('danmakuDensityLimit', ['无', '低', '中', '高']); const closeModal = () => { document.body.removeChild(modal); }; document.getElementById('saveSettings').onclick = () => { try { window.ede.opacity = parseFloatOfRange(document.getElementById('opacity').value, 0, 1); window.localStorage.setItem('danmakuopacity', window.ede.opacity.toString()); showDebugInfo(`设置弹幕透明度:${window.ede.opacity}`); window.ede.speed = parseFloatOfRange(document.getElementById('speed').value, 20, 600); window.localStorage.setItem('danmakuspeed', window.ede.speed.toString()); showDebugInfo(`设置弹幕速度:${window.ede.speed}`); window.ede.fontSize = parseFloatOfRange(document.getElementById('fontSize').value, 8, 40); window.localStorage.setItem('danmakusize', window.ede.fontSize.toString()); showDebugInfo(`设置弹幕大小:${window.ede.fontSize}`); window.ede.heightRatio = parseFloatOfRange(document.getElementById('heightRatio').value, 0, 1); window.localStorage.setItem('danmakuheight', window.ede.heightRatio.toString()); showDebugInfo(`设置弹幕高度:${window.ede.heightRatio}`); window.ede.danmakuFilter = 0; document.querySelectorAll('input[name="danmakuFilter"]:checked').forEach(element => { window.ede.danmakuFilter += parseInt(element.value, 10); }); window.localStorage.setItem('danmakuFilter', window.ede.danmakuFilter); showDebugInfo(`设置弹幕过滤:${window.ede.danmakuFilter}`); window.ede.danmakuModeFilter = 0; document.querySelectorAll('input[name="danmakuModeFilter"]:checked').forEach(element => { window.ede.danmakuModeFilter += parseInt(element.value, 10); }); window.localStorage.setItem('danmakuModeFilter', window.ede.danmakuModeFilter); showDebugInfo(`设置弹幕模式过滤:${window.ede.danmakuModeFilter}`); window.ede.danmakuDensityLimit = parseInt(document.getElementById('danmakuDensityLimit').value); window.localStorage.setItem('danmakuDensityLimit', window.ede.danmakuDensityLimit); showDebugInfo(`设置弹幕密度限制等级:${window.ede.danmakuDensityLimit}`); window.ede.chConvert = parseInt(document.querySelector('input[name="chConvert"]:checked').value); window.localStorage.setItem('chConvert', window.ede.chConvert); showDebugInfo(`设置简繁转换:${window.ede.chConvert}`); window.ede.useXmlDanmaku = parseInt(document.querySelector('input[name="useXmlDanmaku"]:checked').value); window.localStorage.setItem('useXmlDanmaku', window.ede.useXmlDanmaku); showDebugInfo(`是否使用本地xml弹幕:${window.ede.useXmlDanmaku}`); const epOffset = parseFloat(document.getElementById('danmakuOffsetTime').value); window.ede.curEpOffsetModified = epOffset !== window.ede.curEpOffset; if (window.ede.curEpOffsetModified) { window.ede.curEpOffset = epOffset; showDebugInfo(`设置弹幕偏移时间:${window.ede.curEpOffset}`); } window.ede.fontFamily = document.getElementById("danmakuFontFamily").value || "sans-serif"; window.localStorage.setItem('danmakuFontFamily', window.ede.fontFamily); showDebugInfo(`字体:${window.ede.fontFamily}`); window.ede.fontOptions = document.getElementById("danmakuFontOptions").value; window.localStorage.setItem('danmakuFontOptions', window.ede.fontOptions); showDebugInfo(`字体选项:${window.ede.fontOptions}`); reloadDanmaku('reload'); closeModal(); } catch (e) { alert(`Invalid input: ${e.message}`); } }; document.getElementById('cancelSettings').onclick = closeModal; document.getElementById('opacity').oninput = () => showCurrentVal('opacity'); document.getElementById('speed').oninput = () => showCurrentVal('speed'); document.getElementById('fontSize').oninput = () => showCurrentVal('fontSize'); document.getElementById('heightRatio').oninput = () => showCurrentVal('heightRatio'); document.getElementById('danmakuDensityLimit').oninput = () => showCurrentVal('danmakuDensityLimit', ['无', '低', '中', '高']); } }; const sendDanmakuOpts = { title: '发送弹幕', id: 'sendDanmaku', class: send_icon, onclick: () => { // 登录窗口 if (!document.getElementById('loginDialog')) { const modal = document.createElement('div'); modal.id = 'loginDialog'; modal.className = 'dialogContainer'; modal.style.display = 'none'; modal.innerHTML = `
请输入弹弹Play账号密码
账号:
密码:
`; document.body.appendChild(modal); document.getElementById('loginForm').onsubmit = (e) => { e.preventDefault(); const account = document.getElementById('ddPlayAccount').value; const password = document.getElementById('ddPlayPassword').value; if (account && password) { loginDanDanPlay(account, password).then(status => { if (status) { document.getElementById('loginBtn').innerText = '登录✔️'; let sleep = new Promise(resolve => setTimeout(resolve, 1500)); sleep.then(() => { document.getElementById('loginDialog').style.display = 'none'; }); modal.removeEventListener('keydown', event => event.stopPropagation(), true); } }); } }; document.getElementById('cancelBtn').onclick = () => { document.getElementById('loginDialog').style.display = 'none'; modal.removeEventListener('keydown', event => event.stopPropagation(), true); }; } // 发送窗口 if (!document.getElementById('sendDanmakuDialog')) { const modal = document.createElement('div'); modal.id = 'sendDanmakuDialog'; modal.className = 'dialogContainer'; modal.style.display = 'none'; modal.innerHTML = `
`; document.body.appendChild(modal); document.getElementById('sendDanmakuForm').onsubmit = (e) => { e.preventDefault(); const danmakuText = document.getElementById('danmakuText').value; if (danmakuText === '') { const txt = document.getElementById('danmakuText'); txt.placeholder = '弹幕内容不能为空!'; txt.focus(); return; } const _media = document.querySelector(mediaQueryStr); const currentTime = _media.currentTime; const mode = parseInt(document.querySelector('input[name="danmakuMode"]:checked').value); sendDanmaku(danmakuText, currentTime, mode); // 清空输入框的值 document.getElementById('danmakuText').value = ''; modal.style.display = 'none'; modal.removeEventListener('keydown', event => event.stopPropagation(), true); }; document.getElementById('cancelSendDanmakuBtn').onclick = () => { modal.style.display = 'none'; modal.removeEventListener('keydown', event => event.stopPropagation(), true); }; } if (ddplayStatus.isLogin) { const txt = document.getElementById('danmakuText'); txt.placeholder = '请输入弹幕内容'; txt.value = ''; txt.focus(); document.getElementById('sendDanmakuDialog').style.display = 'block'; document.getElementById('sendDanmakuDialog').addEventListener('keydown', event => event.stopPropagation(), true); const animeTitle = window.ede.episode_info ? window.ede.episode_info.animeTitle : ''; const episodeTitle = window.ede.episode_info ? window.ede.episode_info.episodeTitle : ''; document.getElementById('lbAnimeTitle').innerText = `当前番剧: ${animeTitle || ''}`; document.getElementById('lbEpisodeTitle').innerText = `当前集数: ${episodeTitle || ''}`; } else { document.getElementById('loginDialog').style.display = 'block'; document.getElementById('loginDialog').addEventListener('keydown', event => event.stopPropagation(), true); } } }; // ------ 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;i=t[n=s+r>>1][e]?s=n:r=n;return t[s]&&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)}function g(){if(!this._.visible||!this._.paused)return this;if(this._.paused=!1,this.media)for(var t=0;t=0;u--)a=this._.runningList[u],h-(d=this.media?a.time:a._utc)>this._.duration&&(n(this._.stage,a),this._.runningList.splice(u,1));for(var m=[];this._.position=h));)h-d>this._.duration||(this.media&&(a._utc=r-(this.media.currentTime-a.time)),m.push(a)),++this._.position;for(e(this._.stage,m),u=0;u>1),i(this._.stage,a)}}}(this._.engine.framing.bind(this),this._.engine.setup.bind(this),this._.engine.render.bind(this),this._.engine.remove.bind(this));return this._.requestID=a((function t(e){n.call(i,e),i._.requestID=a(t)})),this}function _(){return!this._.visible||this._.paused||(this._.paused=!0,d(this._.requestID),this._.requestID=0),this}function v(){if(!this.media)return this;this.clear(),l(this._.space);var t=u(this.comments,"time",this.media.currentTime);return this._.position=Math.max(0,t-1),this}function w(t){t.play=g.bind(this),t.pause=_.bind(this),t.seeking=v.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 y(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 x(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= 0 && this.danmakuFilter < 16 ? this.danmakuFilter : 0; // 按弹幕模式过滤 const danmakuModeFilter = window.localStorage.getItem('danmakuModeFilter'); this.danmakuModeFilter = danmakuModeFilter ? parseInt(danmakuModeFilter) : 0; this.danmakuModeFilter = this.danmakuModeFilter >= 0 && this.danmakuModeFilter < 8 ? this.danmakuModeFilter : 0; // 弹幕密度限制等级 0:不限制 1:低 2:中 3:高 const danmakuDensityLimit = window.localStorage.getItem('danmakuDensityLimit'); this.danmakuDensityLimit = danmakuDensityLimit ? parseInt(danmakuDensityLimit) : 0; // 使用Jellyfin弹幕插件提供的xml弹幕替代本脚本在线搜索的弹幕 const useXmlDanmaku = window.localStorage.getItem('useXmlDanmaku'); this.useXmlDanmaku = useXmlDanmaku ? parseInt(useXmlDanmaku) : 0; // 当前剧集弹幕偏移时间 this.curEpOffset = 0; this.curEpOffsetModified = false; // 字体 const fontFamily = window.localStorage.getItem('danmakuFontFamily'); this.fontFamily = fontFamily ?? "sans-serif"; // 字体选项 const fontOptions = window.localStorage.getItem('danmakuFontOptions'); this.fontOptions = fontOptions ?? ""; this.danmaku = null; this.episode_info = null; this.obResize = null; this.obMutation = null; this.loading = false; } } const parseFloatOfRange = (str, lb, hb) => { let parsedValue = parseFloat(str); if (isNaN(parsedValue)) { throw new Error('输入无效!'); } return Math.min(Math.max(parsedValue, lb), hb); }; function createButton(opt) { let button = document.createElement('button'); button.className = buttonOptions.class; button.setAttribute('is', buttonOptions.is); button.setAttribute('title', opt.title); button.setAttribute('id', opt.id); let icon = document.createElement('span'); icon.className = spanClass + opt.class; button.appendChild(icon); button.onclick = opt.onclick; return button; } function initListener() { let container = document.querySelector(mediaQueryStr); // 页面未加载 if (!container) { if (window.ede.episode_info) { window.ede.episode_info = null; } return; } } function initUI() { // 页面未加载 let uiAnchor = document.getElementsByClassName(uiAnchorStr); if (!uiAnchor || !uiAnchor[0]) { return; } // 已初始化 if (document.getElementById('danmakuCtr')) { return; } showDebugInfo('正在初始化UI'); // 弹幕按钮容器div let uiEle = null; document.querySelectorAll(uiQueryStr).forEach(function (element) { if (element.offsetParent != null) { uiEle = element.parentNode; } }); if (uiEle == null) { return; } let parent = uiEle.parentNode; let menubar = document.createElement('div'); menubar.id = 'danmakuCtr'; if (!window.ede.episode_info) { menubar.style.opacity = 0.5; } parent.insertBefore(menubar, uiEle.nextSibling); // 弹幕开关 displayButtonOpts.class = danmaku_icons[window.ede.danmakuSwitch]; menubar.appendChild(createButton(displayButtonOpts)); // 手动匹配 menubar.appendChild(createButton(searchButtonOpts)); // 手动增加弹幕源 menubar.appendChild(createButton(sourceButtonOpts)); // 弹幕设置 menubar.appendChild(createButton(settingButtonOpts)); // 日志开关 logButtonOpts.class = log_icons[window.ede.logSwitch]; menubar.appendChild(createButton(logButtonOpts)); // 发送弹幕 menubar.appendChild(createButton(sendDanmakuOpts)); let _container = null; document.querySelectorAll(mediaContainerQueryStr).forEach(function (element) { if (!element.classList.contains('hide')) { _container = element; } }); let span = document.createElement('span'); span.id = 'debugInfo'; span.style.position = 'absolute'; span.style.overflow = 'auto'; span.style.zIndex = '99'; span.style.right = '50px'; span.style.top = '50px'; span.style.background = 'rgba(28, 28, 28, .8)'; span.style.color = '#fff'; span.style.padding = '20px'; span.style.borderRadius = '.3em'; span.style.maxHeight = '50%' window.ede.logSwitch == 1 ? (span.style.display = 'block') : (span.style.display = 'none'); _container.appendChild(span); showDebugInfo('UI初始化完成'); reloadDanmaku('init'); refreshDanDanPlayToken(); } async function loginDanDanPlay(account, passwd) { const loginUrl = authPrefix + '/api/v2/login'; const params = { 'userName': account, 'password': passwd }; try { const resp = await fetch(loginUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': navigator.userAgent }, body: JSON.stringify(params) }); if (resp.status !== 200) { showDebugInfo('登录失败 http error:' + resp.status); alert('登录失败 http error:' + resp.status); return false; } const json = await resp.json(); if (json.errorCode !== 0) { showDebugInfo('登录失败 ' + json.errorMessage); alert('登录失败 ' + json.errorMessage); return false; } ddplayStatus.isLogin = true; ddplayStatus.token = json.token; ddplayStatus.tokenExpire = json.tokenExpireTime; window.localStorage.setItem('ddplayStatus', JSON.stringify(ddplayStatus)); showDebugInfo('登录成功'); return true; } catch (error) { console.error('登录失败', error); alert('登录失败'); return false; } } async function refreshDanDanPlayToken() { if (ddplayStatus.isLogin) { const now = Math.floor(Date.now() / 1000); const expire = new Date(ddplayStatus.tokenExpire).getTime() / 1000; if (expire < now) { ddplayStatus.isLogin = false; return; } else if (expire - now > 259200) { // Token expires in more than 3 days, no need to refresh return; } else { // Refresh token before 3 days const refreshUrl = apiPrefix + '/api/v2/login/renew'; try { const resp = await fetch(refreshUrl, { method: 'GET', headers: { 'Accept': 'application/json', 'User-Agent': navigator.userAgent, 'Authorization': 'Bearer ' + ddplayStatus.token } }); if (resp.status !== 200) { showDebugInfo('刷新弹弹Play Token失败 http error:' + resp.status); return; } const json = await resp.json(); if (json.errorCode === 0) { ddplayStatus.isLogin = true; ddplayStatus.token = json.token; ddplayStatus.tokenExpire = json.tokenExpireTime; } else { showDebugInfo('刷新弹弹Play Token失败'); showDebugInfo(json.errorMessage); } } catch (error) { console.error('刷新弹弹Play Token失败', error); } } } } async function sendDanmaku(danmakuText, time, mode = 1, color = 0xffffff) { if (ddplayStatus.isLogin) { if (!window.ede.episode_info || !window.ede.episode_info.episodeId) { showDebugInfo('发送弹幕失败 未获取到弹幕信息'); alert('请先获取弹幕信息'); return; } const danmakuUrl = apiPrefix + '/api/v2/comment/' + window.ede.episode_info.episodeId; const params = { 'time': time, 'mode': mode, 'color': color, 'comment': danmakuText }; try { const resp = await fetch(danmakuUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': navigator.userAgent, 'Authorization': 'Bearer ' + ddplayStatus.token }, body: JSON.stringify(params) }); if (resp.status !== 200) { showDebugInfo('发送弹幕失败 http error:' + resp.status); return; } const json = await resp.json(); if (json.errorCode === 0) { const colorStr = `000000${color.toString(16)}`.slice(-6); const modemap = { 6: 'ltr', 1: 'rtl', 5: 'top', 4: 'bottom' }[mode]; const comment = { text: danmakuText, mode: modemap, time: time, style: { font: `${window.ede.fontOptions} ${window.ede.fontSize}px ${window.ede.fontFamily}`, fillStyle: `#${colorStr}`, strokeStyle: colorStr === '000000' ? '#fff' : '#000', lineWidth: 2.0, }, }; window.ede.danmaku.emit(comment); showDebugInfo('发送弹幕成功'); } else { showDebugInfo('发送弹幕失败'); showDebugInfo(json.errorMessage); alert('发送失败:' + json.errorMessage); } } catch (error) { console.error('发送弹幕失败', error); showDebugInfo('发送弹幕失败'); } } } async function postRelatedSource(relatedUrl) { if (!ddplayStatus.isLogin) { showDebugInfo('发送相关链接失败 未登录'); alert('请先登录'); return; } if (!window.ede.episode_info || !window.ede.episode_info.episodeId) { showDebugInfo('发送弹幕失败 未获取到弹幕信息'); alert('请先获取弹幕信息'); return; } const url = apiPrefix + '/api/v2/related/' + window.ede.episode_info.episodeId; const params = { 'episodeId': window.ede.episode_info.episodeId, 'url': relatedUrl, 'shift': 0 }; try { const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': navigator.userAgent, 'Authorization': 'Bearer ' + ddplayStatus.token }, body: JSON.stringify(params) }); if (resp.status !== 200) { showDebugInfo('发送相关链接失败 http error:' + resp.code); return; } const json = await resp.json(); if (json.errorCode === 0) { showDebugInfo('发送相关链接成功'); } else { showDebugInfo('发送相关链接失败'); showDebugInfo(json.errorMessage); alert('弹幕源提交弹弹Play失败:' + json.errorMessage); } } catch (error) { console.error('发送相关链接失败', error); showDebugInfo('发送相关链接失败'); } } async function showDebugInfo(msg) { let span = document.getElementById('debugInfo'); while (!span) { await new Promise((resolve) => setTimeout(resolve, 200)); span = document.getElementById('debugInfo'); } let msgStr = msg; if (typeof msg !== 'string') { msgStr = JSON.stringify(msg); } let lastLine = span.innerHTML.slice(span.innerHTML.lastIndexOf('
') + 4); let baseLine = lastLine.replace(/ X\d+$/, ''); if (baseLine === msgStr) { let count = 2; if (lastLine.match(/ X(\d+)$/)) { count = parseInt(lastLine.match(/ X(\d+)$/)[1]) + 1; } msgStr = `${msgStr} X${count}`; span.innerHTML = span.innerHTML.slice(0, span.innerHTML.lastIndexOf('
') + 4) + msgStr; } else { span.innerHTML += span.innerHTML === '' ? msgStr : '
' + msgStr; } console.log(msg); } async function getEmbyItemInfo() { let playingInfo = null; while (!playingInfo) { await new Promise((resolve) => setTimeout(resolve, 200)); if (isNewJellyfin) { // params: userId, itemId playingInfo = await ApiClient.getItem(ApiClient.getCurrentUserId(), itemId); } else { let sessionInfo = await ApiClient.getSessions({ userId: ApiClient.getCurrentUserId(), deviceId: ApiClient.deviceId(), }); if (!sessionInfo[0].NowPlayingItem) { await new Promise(resolve => setTimeout(resolve, 150)); continue; } playingInfo = sessionInfo[0].NowPlayingItem; } } showDebugInfo('获取Item信息成功: ' + (playingInfo.SeriesName || playingInfo.Name)); return playingInfo; } function makeGetRequest(url) { return fetch(url, { method: 'GET', headers: { "Accept-Encoding": "gzip,br", "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; _id = item.SeasonId || item.Id; animeName = item.SeriesName || item.Name; episode = item.IndexNumber || 1; let session = item.ParentIndexNumber; if (session > 1) { animeName += session; } 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)) { const episodeInfo = JSON.parse(window.localStorage.getItem(_episode_key)); return episodeInfo; } } 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); if (animeName == null || animeName == '') { return null; } } const _episode_key_offset = _episode_key + '_offset'; if (window.ede.curEpOffsetModified) { window.localStorage.setItem(_episode_key_offset, window.ede.curEpOffset); } window.ede.curEpOffset = window.localStorage.getItem(_episode_key_offset) || 0; let searchUrl = apiPrefix + '/api/v2/search/episodes?anime=' + animeName; let animaInfo = await makeGetRequest(searchUrl) .then((response) => response.json()) .catch((error) => { showDebugInfo('查询失败:', error); return null; }); if (animaInfo.animes.length == 0) { const seriesInfo = await ApiClient.getItem(ApiClient.getCurrentUserId(), item.SeriesId || item.Id); animeName = seriesInfo.OriginalTitle; if (animeName?.length > 0) { searchUrl = apiPrefix + '/api/v2/search/episodes?anime=' + animeName; animaInfo = await makeGetRequest(searchUrl) .then((response) => response.json()) .catch((error) => { showDebugInfo('查询失败:', error); return null; }); } } if (animaInfo.animes.length == 0) { showDebugInfo('弹幕查询无结果'); return null; } showDebugInfo('节目查询成功'); 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) || 1); if (episode == null || episode == '') { return null; } episode = parseInt(episode) - 1; } else { selecAnime_id = parseInt(selecAnime_id) - 1; let initialTitle = animaInfo.animes[selecAnime_id].episodes[0].episodeTitle; const match = initialTitle.match(/第(\d+)话/); const initialep = match ? parseInt(match[1]) : 1; episode = (parseInt(episode) < initialep) ? parseInt(episode) - 1 : (parseInt(episode) - initialep); } if (episode + 1 > animaInfo.animes[selecAnime_id].episodes.length) { showDebugInfo('剧集不存在'); return null; } const epTitlePrefix = animaInfo.animes[selecAnime_id].type === 'tvseries' ? `S${session}E${episode + 1}` : (animaInfo.animes[selecAnime_id].type); let episodeInfo = { episodeId: animaInfo.animes[selecAnime_id].episodes[episode].episodeId, animeTitle: animaInfo.animes[selecAnime_id].animeTitle, episodeTitle: epTitlePrefix + ' ' + animaInfo.animes[selecAnime_id].episodes[episode].episodeTitle, }; window.localStorage.setItem(_episode_key, JSON.stringify(episodeInfo)); return episodeInfo; } async function getComments(episodeId) { const { danmakuFilter } = window.ede; const url_all = apiPrefix + '/api/v2/comment/' + episodeId + '?withRelated=true&chConvert=' + window.ede.chConvert; const url_related = apiPrefix + '/api/v2/related/' + episodeId; const url_ext = apiPrefix + '/api/v2/extcomment?url='; try { let response = await makeGetRequest(url_all); let data = await response.json(); const matchBili = /^\[BiliBili\]/; let hasBili = false; if ((danmakuFilter & 1) !== 1) { for (const c of data.comments) { if (matchBili.test(c.p.split(',').pop())) { hasBili = true; break; } } } let comments = data.comments; response = await makeGetRequest(url_related); data = await response.json(); showDebugInfo('第三方弹幕源个数:' + data.relateds.length); if (data.relateds.length > 0) { // 根据设置过滤弹幕源 let src = []; for (const s of data.relateds) { if ((danmakuFilter & 1) !== 1 && !hasBili && s.url.includes('bilibili.com/bangumi')) { src.push(s.url); } if ((danmakuFilter & 1) !== 1 && s.url.includes('bilibili.com/video')) { src.push(s.url); } if ((danmakuFilter & 2) !== 2 && s.url.includes('gamer')) { src.push(s.url); } if ((danmakuFilter & 8) !== 8 && !s.url.includes('bilibili') && !s.url.includes('gamer')) { src.push(s.url); } } // 获取第三方弹幕 await Promise.all(src.map(async (s) => { const response = await makeGetRequest(url_ext + encodeURIComponent(s)); const data = await response.json(); comments = comments.concat(data.comments); })); } showDebugInfo('弹幕下载成功: ' + comments.length); return comments; } catch (error) { showDebugInfo('获取弹幕失败:', error); return null; } } async function getCommentsByUrl(src) { const url_encoded = encodeURIComponent(src); const url = apiPrefix + '/api/v2/extcomment?url=' + url_encoded; for (let i = 0; i < 2; i++) { try { const response = await makeGetRequest(url); const data = await response.json(); showDebugInfo('弹幕下载成功: ' + data.comments.length); return data.comments; } catch (error) { showDebugInfo('获取弹幕失败:', error); } } return null; } async function getItemId() { let item = await getEmbyItemInfo(); if (!item) { return null; } return item.Id || null; } async function getCommentsByPluginApi(jellyfinItemId) { const path = window.location.pathname.replace(/\/web\/(index\.html)?/, '/api/danmu/'); const url = window.location.origin + path + jellyfinItemId + '/raw'; const response = await fetch(url); if (!response.ok) { return null; } const xmlText = await response.text(); if (!xmlText || xmlText.length === 0) { return null; } // parse the xml data // xml data: 弹幕内容 // content // comment data: {cid: "1723088443", p: "392.00,1,16777215,[BiliBili]e6860b30", m: "弹幕内容"} // {cid: "dbid", p: "stime, type, color, sender", m: "content"} try { const parser = new DOMParser(); const data = parser.parseFromString(xmlText, 'text/xml'); const comments = []; for (const comment of data.getElementsByTagName('d')) { const p = comment.getAttribute('p').split(',').map(Number); const commentData = { cid: p[7], p: p[0] + ',' + p[1] + ',' + p[3] + ',' + p[6], m: comment.textContent }; comments.push(commentData); } return comments; } catch (error) { return null; } } async function createDanmaku(comments) { if (!window.obVideo) { window.obVideo = new MutationObserver((mutationList, _observer) => { for (let mutationRecord of mutationList) { if (mutationRecord.removedNodes) { for (let removedNode of mutationRecord.removedNodes) { if (removedNode.className && removedNode.classList.contains('videoPlayerContainer')) { console.log('[Jellyfin-Danmaku] Video Removed'); window.ede.loading = false; document.getElementById('danmakuInfoTitle')?.remove(); const wrapper = document.getElementById('danmakuWrapper'); if (wrapper) wrapper.style.display = 'none'; return; } } } if (mutationRecord.addedNodes) { for (let addedNode of mutationRecord.addedNodes) { if (addedNode.className && addedNode.classList.contains('videoPlayerContainer')) { console.log('[Jellyfin-Danmaku] Video Added'); reloadDanmaku('refresh'); return; } } } } }); window.obVideo.observe(document.body, { childList: true }); } if (!comments) { showDebugInfo('无弹幕'); return; } let wrapper = document.getElementById('danmakuWrapper'); wrapper && wrapper.remove(); if (window.ede.danmaku) { window.ede.danmaku.clear(); window.ede.danmaku.destroy(); window.ede.danmaku = null; } let _comments = danmakuFilter(danmakuParser(comments)); showDebugInfo(`弹幕加载成功: ${_comments.length}`); showDebugInfo(`弹幕透明度:${window.ede.opacity}`); showDebugInfo(`弹幕速度:${window.ede.speed}`); showDebugInfo(`弹幕高度比例:${window.ede.heightRatio}`); showDebugInfo(`弹幕来源过滤:${window.ede.danmakuFilter}`); showDebugInfo(`弹幕模式过滤:${window.ede.danmakuModeFilter}`); showDebugInfo(`弹幕字号:${window.ede.fontSize}`); showDebugInfo(`弹幕字体:${window.ede.fontFamily}`); showDebugInfo(`弹幕字体选项:${window.ede.fontOptions}`); showDebugInfo(`屏幕分辨率:${window.screen.width}x${window.screen.height}`); if (window.ede.curEpOffset !== 0) showDebugInfo(`当前弹幕偏移:${window.ede.curEpOffset} 秒`); const waitForMediaContainer = async () => { while (!document.querySelector(mediaContainerQueryStr)) { await new Promise((resolve) => setTimeout(resolve, 200)); } }; await waitForMediaContainer(); let _container = null; const reactRoot = document.getElementById('reactRoot'); document.querySelectorAll(mediaContainerQueryStr).forEach((element) => { if (!element.classList.contains('hide')) { _container = element; } }); if (!_container) { showDebugInfo('未找到播放器'); return; } let _media = document.querySelector(mediaQueryStr); if (!_media) { showDebugInfo('未找到video'); return; } wrapper = document.createElement('div'); wrapper.id = 'danmakuWrapper'; wrapper.style.position = 'fixed'; wrapper.style.width = '100%'; wrapper.style.height = `calc(${window.ede.heightRatio * 100}% - 18px)`; wrapper.style.opacity = window.ede.opacity; wrapper.style.top = '18px'; wrapper.style.pointerEvents = 'none'; if (reactRoot) { reactRoot.prepend(wrapper); } else { _container.prepend(wrapper); } window.ede.danmaku = new Danmaku({ container: wrapper, media: _media, comments: _comments, engine: 'canvas', speed: window.ede.speed, }); window.ede.danmakuSwitch === 1 ? window.ede.danmaku.show() : window.ede.danmaku.hide(); const resizeObserverCallback = () => { if (window.ede.danmaku) { showDebugInfo('重设容器大小'); window.ede.danmaku.resize(); } }; if (window.ede.obResize) { window.ede.obResize.disconnect(); } window.ede.obResize = new ResizeObserver(resizeObserverCallback); window.ede.obResize.observe(_container); const mutationObserverCallback = () => { if (window.ede.danmaku && document.querySelector(mediaQueryStr)) { showDebugInfo('探测播放媒体变化'); const sleep = new Promise(resolve => setTimeout(resolve, 3000)); sleep.then(() => reloadDanmaku('refresh')); } }; if (window.ede.obMutation) { window.ede.obMutation.disconnect(); } window.ede.obMutation = new MutationObserver(mutationObserverCallback); window.ede.obMutation.observe(_media, { attributes: true }); } function displayDanmakuInfo(info) { let infoContainer = document.getElementById('danmakuInfoTitle'); if (!infoContainer) { infoContainer = document.createElement('div'); infoContainer.id = 'danmakuInfoTitle'; infoContainer.className = 'pageTitle'; document.querySelector('div.skinHeader').appendChild(infoContainer); } infoContainer.innerText = `弹幕匹配信息:${info.animeTitle} - ${info.episodeTitle}`; } function reloadDanmaku(type = 'check') { if (window.ede.loading) { showDebugInfo('正在重新加载'); return; } window.ede.loading = true; if (window.ede.useXmlDanmaku === 1) { getItemId().then((itemId) => { return new Promise((resolve, reject) => { if (!itemId) { if (type != 'init') { reject('播放器未完成加载'); } else { reject(null); } } resolve(itemId); }); }).then((itemId) => getCommentsByPluginApi(itemId)) .then((comments) => { if (comments?.length > 0) { return createDanmaku(comments).then(() => { showDebugInfo('本地弹幕就位'); }).then(() => { window.ede.loading = false; const danmakuCtr = document.getElementById('danmakuCtr'); if (danmakuCtr && danmakuCtr.style && danmakuCtr.style.opacity !== '1') { danmakuCtr.style.opacity = 1; } }); } throw new Error('本地弹幕加载失败,尝试在线加载'); }) .catch((error) => { showDebugInfo(error.message); return loadOnlineDanmaku(type); }); } else { loadOnlineDanmaku(type); } } function loadOnlineDanmaku(type) { return 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; displayDanmakuInfo(info); resolve(info.episodeId); } }); }) .then((episodeId) => getComments(episodeId).then((comments) => createDanmaku(comments).then(() => { showDebugInfo('弹幕就位'); }), ), (msg) => { if (msg) { showDebugInfo(msg); } }, ) .then(() => { window.ede.loading = false; const danmakuCtr = document.getElementById('danmakuCtr'); if (danmakuCtr && danmakuCtr.style && danmakuCtr.style.opacity !== '1') { danmakuCtr.style.opacity = 1; } }); } function danmakuFilter(comments) { const level = window.ede.danmakuDensityLimit; if (level === 0) { return comments; } let _container = null; document.querySelectorAll(mediaContainerQueryStr).forEach((element) => { if (!element.classList.contains('hide')) { _container = element; } }); const containerWidth = _container.offsetWidth; const containerHeight = _container.offsetHeight * window.ede.heightRatio - 18; const duration = Math.ceil(containerWidth / window.ede.speed); const lines = Math.floor(containerHeight / window.ede.fontSize) - 1; const limit = (9 - level * 2) * lines; const verticalLimit = lines - 1 > 0 ? lines - 1 : 1; const resultComments = []; const timeBuckets = {}; const verticalTimeBuckets = {}; comments.forEach(comment => { const timeIndex = Math.ceil(comment.time / duration); if (!timeBuckets[timeIndex]) { timeBuckets[timeIndex] = 0; } if (!verticalTimeBuckets[timeIndex]) { verticalTimeBuckets[timeIndex] = 0; } if (comment.mode === 'top' || comment.mode === 'bottom') { if (verticalTimeBuckets[timeIndex] < verticalLimit) { verticalTimeBuckets[timeIndex]++; resultComments.push(comment); } } else { if (timeBuckets[timeIndex] < limit) { timeBuckets[timeIndex]++; resultComments.push(comment); } } }); return resultComments; } function danmakuParser(all_cmts) { const { fontSize, fontOptions, fontFamily, danmakuFilter, danmakuModeFilter, curEpOffset } = window.ede; const disableBilibili = (danmakuFilter & 1) === 1; const disableGamer = (danmakuFilter & 2) === 2; const disableDandan = (danmakuFilter & 4) === 4; const disableOther = (danmakuFilter & 8) === 8; let filterule = ''; if (disableDandan) { filterule += '^(?!\\[)|\^.{0,3}\\]'; } if (disableBilibili) { filterule += (filterule ? '|' : '') + '\^\\[BiliBili\\]'; } if (disableGamer) { filterule += (filterule ? '|' : '') + '\^\\[Gamer\\]'; } if (disableOther) { filterule += (filterule ? '|' : '') + '\^\\[\(\?\!\(BiliBili\|Gamer\)\).{3,}\\]'; } if (filterule === '') { filterule = '!.*'; } const danmakuFilteRule = new RegExp(filterule); // 使用Map去重 const unique_cmts = []; const cmtMap = new Map(); const removeUserRegex = /,[^,]+$/; //p: time,modeId,colorValue,user all_cmts.forEach((comment) => { const p = comment.p.replace(removeUserRegex, ''); if (!cmtMap.has(p + comment.m)) { cmtMap.set(p + comment.m, true); unique_cmts.push(comment); } }); let enabledMode = [1, 4, 5, 6]; if ((danmakuModeFilter & 1) === 1) { enabledMode = enabledMode.filter((v) => v !== 4); } if ((danmakuModeFilter & 2) === 2) { enabledMode = enabledMode.filter((v) => v !== 5); } if ((danmakuModeFilter & 4) === 4) { enabledMode = enabledMode.filter((v) => v !== 6 && v !== 1); } return unique_cmts .filter((comment) => { const user = comment.p.split(',')[3]; const modeId = parseInt(comment.p.split(',')[1], 10); return !danmakuFilteRule.test(user) && enabledMode.includes(modeId); }) .map((comment) => { const [time, modeId, colorValue] = comment.p.split(',').map((v, i) => i === 0 ? parseFloat(v) : parseInt(v, 10)); const mode = { 1: 'rtl', 4: 'bottom', 5: 'top', 6: 'ltr' }[modeId]; const color = colorValue.toString(16).padStart(6, '0'); return { text: comment.m, mode, time: time + curEpOffset, style: { font: `${fontOptions} ${fontSize}px ${fontFamily}`, fillStyle: `#${color}`, strokeStyle: color === '000000' ? '#fff' : '#000', lineWidth: 2.0, }, }; }); } 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; } const waitForElement = (selector) => { return new Promise((resolve) => { const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); }); }; const compareVersions = (version1, version2) => { if (typeof version1 !== 'string') return -1; if (typeof version2 !== 'string') return 1; const v1 = version1.split('.').map(Number); const v2 = version2.split('.').map(Number); for (let i = 0; i < Math.max(v1.length, v2.length); i++) { const n1 = v1[i] || 0; const n2 = v2[i] || 0; if (n1 > n2) return 1; if (n1 < n2) return -1; } return 0; } waitForElement('.htmlvideoplayer').then(() => { if (!window.ede) { window.ede = new EDE(); const materialIcon = document.querySelector('.material-icons'); const fontFamily = window.getComputedStyle(materialIcon).fontFamily; if (fontFamily === '"Font Awesome 6 Pro"') { danmaku_icons = ['fa-comment-slash', 'fa-comment']; log_icons = ['fa-toilet-paper-slash', 'fa-toilet-paper']; searchButtonOpts.class = 'fa-search'; sourceButtonOpts.class = 'fa-square-plus'; settingButtonOpts.class = 'fa-sliders'; sendDanmakuOpts.class = 'fa-paper-plane'; } (async () => { isNewJellyfin = compareVersions(ApiClient?._appVersion, '10.10.0') >= 0; // showDebugInfo(`isNewJellyfin: ${isNewJellyfin}`); if (isNewJellyfin) { let retry = 0; while (!itemId) { await new Promise((resolve) => setTimeout(resolve, 200)); retry++; if (retry > 10) { throw new Error('获取itemId失败'); } } } else { while (!(await ApiClient.getSessions())) { await new Promise((resolve) => setTimeout(resolve, 200)); } } setInterval(() => { initUI(); }, check_interval); setInterval(() => { initListener(); }, check_interval); })(); } }); })();