From 6a92e8448801ce2809e9f7df1046b4c163cf933f Mon Sep 17 00:00:00 2001 From: Izumiko Date: Sat, 31 Aug 2024 21:30:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=A0=E8=BD=BD=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0xml=E5=BC=B9=E5=B9=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++- ede.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 79fbbbf..b87a2ba 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,13 @@ - 弹幕设置: - 设置弹幕透明度 [0, 1] - 设置弹幕速度 [20, 600] - - 设置弹幕字体大小 [8, 32] + - 设置弹幕字体大小 [8, 80] - 设置弹幕区域占屏幕的高度比例 [0, 1] - 弹幕密度: 依据水平和垂直密度过滤, 弹幕0级无限制* - 设置弹幕用户名过滤,支持选项: 哔哩哔哩, 巴哈姆特, 弹弹Play, 其他 - 设置弹幕类型过滤,支持:底部,顶部,滚动 - 简繁转换: 在原始弹幕/简体中文/繁体中文3种模式切换 + - 是否使用本地XML弹幕 - 当前集数弹幕偏移时间 - 日志开关: 开启/关闭调试日志输出 - 发送弹幕: 登录弹弹Play,并在播放界面发送弹幕 @@ -38,6 +39,8 @@ 弹幕来源为 [弹弹 play](https://www.dandanplay.com/) ,已开启弹幕聚合(Acfun/Bili/Tucao/Baha/5DM/iQIYI等不知名网站弹幕融合) +**在启用了`使用本地xml弹幕`后,会尝试调用[cxfksword/jellyfin-plugin-danmu](https://github.com/cxfksword/jellyfin-plugin-danmu)的API获取其预先下载好的xml弹幕,绕过弹弹play的弹幕查询和加载** + ## 数据 匹配完成后对应关系会保存在**浏览器(或客户端)本地存储**中,后续播放(包括同季的其他集)会优先按照保存的匹配记录载入弹幕 diff --git a/ede.js b/ede.js index adc6bbe..467bac8 100644 --- a/ede.js +++ b/ede.js @@ -3,7 +3,7 @@ // @description Jellyfin弹幕插件 // @namespace https://github.com/RyoLee // @author RyoLee -// @version 1.43 +// @version 1.44 // @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 @@ -154,7 +154,7 @@
字体大小: - +
高度比例: @@ -193,6 +193,13 @@
+
+ +
+
+
+
+
@@ -258,6 +265,9 @@ 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) { @@ -459,6 +469,9 @@ // 弹幕密度限制等级 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; @@ -722,6 +735,16 @@ } 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, @@ -1018,6 +1041,44 @@ 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\//, '/api/danmu/'); + const url = window.location.origin + path + jellyfinItemId + '/raw'; + const response = await makeGetRequest(url); + if (!response || response.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"} + const parser = new DOMParser(); + const data = parser.parseFromString(response, '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; + } + async function createDanmaku(comments) { if (!window.obVideo) { window.obVideo = new MutationObserver((mutationList, _observer) => { @@ -1166,12 +1227,41 @@ infoContainer.innerText = `弹幕匹配信息:${info.animeTitle} - ${info.episodeTitle}`; } - function reloadDanmaku(type = 'check') { + async function reloadDanmaku(type = 'check') { if (window.ede.loading) { showDebugInfo('正在重新加载'); return; } window.ede.loading = true; + if (window.ede.useXmlDanmaku === 1) { + const comments = await getItemId().then((itemId) => { + return new Promise((resolve, reject) => { + if (!itemId) { + if (type != 'init') { + reject('播放器未完成加载'); + } else { + reject(null); + } + } + resolve(itemId); + }); + }).then((itemId) => getCommentsByPluginApi(itemId)); + + if (comments.length > 0) { + 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; + } + }); + return; + } + + showDebugInfo('本地弹幕加载失败,尝试在线加载'); + } getEpisodeInfo(type != 'search') .then((info) => { return new Promise((resolve, reject) => {