├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── DEPLOY.yml ├── .pre-commit-config.yaml ├── .prettierrc.json ├── LICENSE ├── README.md ├── cf_worker.js └── ede.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG]' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Version** 10 | 11 | - emby version. 12 | - emby-danmaku version. 13 | 14 | **Describe the bug** 15 | 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | 20 | Steps to reproduce the behavior: 21 | 22 | 1. Go to '...' 23 | 2. Click on '....' 24 | 3. See error 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[UR/FR]' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/DEPLOY.yml: -------------------------------------------------------------------------------- 1 | name: DEPLOY 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | paths: 7 | - "ede.js" 8 | push: 9 | branches: 10 | - master 11 | paths: 12 | - "ede.js" 13 | workflow_dispatch: 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | if: github.ref == 'refs/heads/master' 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: Pack 24 | run: | 25 | rm -rf public 26 | mkdir -p public 27 | cp ede.js public/ede.user.js 28 | - name: Deploy github pages 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./public 33 | keep_files: true 34 | - name: Purge CDN cache 35 | run: | 36 | curl https://purge.jsdelivr.net/gh/RyoLee/emby-danmaku/ede.js 37 | curl https://purge.jsdelivr.net/gh/RyoLee/emby-danmaku@gh-pages/ede.user.js -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | 2 | ci: 3 | autofix_prs: true 4 | autoupdate_branch: develop 5 | autoupdate_schedule: weekly 6 | 7 | repos: 8 | - repo: https://github.com/pre-commit/mirrors-prettier 9 | rev: v3.0.0-alpha.6 10 | hooks: 11 | - id: prettier 12 | types: [javascript] -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "requirePragma": false, 4 | "insertPragma": false, 5 | "useTabs": false, 6 | "tabWidth": 4, 7 | "semi": true, 8 | "proseWrap": "preserve", 9 | "endOfLine": "lf", 10 | "printWidth": 180, 11 | "trailingComma": "all" 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emby-danmaku 2 | 3 | ## Emby danmaku extension 4 | ![截图](https://raw.githubusercontent.com/RyoLee/emby-danmaku/res/S0.png) 5 | 6 | ## 安装 7 | 8 | 任选以下一种方式安装即可 9 | 10 | ### 浏览器插件(推荐) 11 | 12 | 1. [Tampermonkey](https://www.tampermonkey.net/) 13 | 2. [添加脚本](https://cdn.jsdelivr.net/gh/RyoLee/emby-danmaku@gh-pages/ede.user.js) 14 | 15 | ### 修改服务端 16 | 17 | 修改文件 /system/dashboard-ui/index.html (Docker版,其他类似),在``前添加如下标签 18 | 19 | ``` 20 | 21 | ``` 22 | 该方式安装与浏览器插件安装**可同时使用不冲突** 23 | 24 | ### 修改客户端 25 | 26 | 类似服务端方式,解包后修改 dashboard-ui/index.html 再重新打包即可,iOS 需要通过类似 AltStore 方式自签,请自行 Google 解决 27 | 28 | ## 界面 29 | 30 | **请注意Readme上方截图可能与最新版存在差异,请以实际版本与说明为准** 31 | 32 | 左下方新增如下按钮,若按钮透明度与"暂停"等其他原始按钮存在差异,说明插件正在进行加载 33 | 34 | - 弹幕开关: 切换弹幕显示/隐藏状态 35 | - 手动匹配: 手动输入信息匹配弹幕 36 | - 简繁转换: 在原始弹幕/简体中文/繁体中文3种模式切换 37 | - 过滤等级: 过滤弹幕强度,等级越高强度越大,0级无限制* 38 | - 弹幕信息: 通过通知(以及后台log)显示当前匹配弹幕信息 39 | 40 | **除0级外均带有每3秒6条的垂直方向弹幕密度限制,高于该限制密度的顶部/底部弹幕将会被转为普通弹幕* 41 | 42 | ## 弹幕 43 | 44 | 弹幕来源为 [弹弹 play](https://www.dandanplay.com/) ,已开启弹幕聚合(A/B/C 站等网站弹幕融合) 45 | 46 | ## 数据 47 | 48 | 匹配完成后对应关系会保存在**浏览器(或客户端)本地存储**中,后续播放(包括同季的其他集)会优先按照保存的匹配记录载入弹幕 49 | 50 | ## 常见弹幕加载错误/失败原因 51 | 52 | 1. 译名导致的异常: 如『よふかしのうた』 Emby 识别为《彻夜之歌》后因为弹弹 play 中为《夜曲》导致无法匹配 53 | 2. 存在多季/剧场版/OVA 等导致的异常: 如『OVERLORD』第四季若使用S[N]格式归档(如OVERLORD/S4E1.mkv或OVERLORD/S4/E1.mkv),可能出现匹配失败/错误等现象 54 | 3. 其他加载BUG: ~~鉴定为后端程序猿不会前端还要硬写JS~~,有BUG麻烦 [开个issue](https://github.com/RyoLee/emby-danmaku/issues/new/choose) THX 55 | 56 | **首次播放时请检查当前弹幕信息是否正确匹配,若匹配错误请尝试手动匹配** 57 | 58 | 59 | [![Stargazers over time](https://starchart.cc/RyoLee/emby-danmaku.svg)](https://starchart.cc/RyoLee/emby-danmaku) 60 | -------------------------------------------------------------------------------- /cf_worker.js: -------------------------------------------------------------------------------- 1 | const hostlist = { 'api.dandanplay.net': null }; 2 | 3 | async function handleRequest(request) { 4 | const urlObj = new URL(request.url); 5 | let url = urlObj.href.replace(urlObj.origin + '/cors/', '').trim(); 6 | if (0 !== url.indexOf('https://') && 0 === url.indexOf('https:')) { 7 | url = url.replace('https:/', 'https://'); 8 | } else if (0 !== url.indexOf('http://') && 0 === url.indexOf('http:')) { 9 | url = url.replace('http:/', 'http://'); 10 | } 11 | let tUrlObj = new URL(url); 12 | if (!(tUrlObj.hostname in hostlist)) { 13 | return Forbidden(tUrlObj); 14 | } 15 | let response = await fetch(url, { 16 | headers: request.headers, 17 | body: request.body, 18 | method: request.method, 19 | }); 20 | response = new Response(await response.body, response); 21 | response.headers.set('Access-Control-Allow-Origin', '*'); 22 | return response; 23 | } 24 | 25 | function Forbidden(url) { 26 | return new Response(`Hostname ${url.hostname} not allowed.`, { 27 | status: 403, 28 | }); 29 | } 30 | 31 | addEventListener('fetch', (event) => { 32 | return event.respondWith(handleRequest(event.request)); 33 | }); 34 | -------------------------------------------------------------------------------- /ede.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Emby danmaku extension 3 | // @description Emby弹幕插件 4 | // @namespace https://github.com/RyoLee 5 | // @author RyoLee 6 | // @version 1.11 7 | // @copyright 2022, RyoLee (https://github.com/RyoLee) 8 | // @license MIT; https://raw.githubusercontent.com/RyoLee/emby-danmaku/master/LICENSE 9 | // @icon https://github.githubassets.com/pinned-octocat.svg 10 | // @updateURL https://cdn.jsdelivr.net/gh/RyoLee/emby-danmaku@gh-pages/ede.user.js 11 | // @downloadURL https://cdn.jsdelivr.net/gh/RyoLee/emby-danmaku@gh-pages/ede.user.js 12 | // @grant none 13 | // @match */web/index.html 14 | // ==/UserScript== 15 | 16 | (async function () { 17 | 'use strict'; 18 | if (document.querySelector('meta[name="application-name"]').content == 'Emby') { 19 | // ------ configs start------ 20 | const check_interval = 200; 21 | const chConverTtitle = ['当前状态: 未启用', '当前状态: 转换为简体', '当前状态: 转换为繁体']; 22 | // 0:当前状态关闭 1:当前状态打开 23 | const danmaku_icons = ['\uE0B9', '\uE7A2']; 24 | const search_icon = '\uE881'; 25 | const translate_icon = '\uE927'; 26 | const info_icon = '\uE0E0'; 27 | const filter_icons = ['\uE3E0', '\uE3D0', '\uE3D1', '\uE3D2']; 28 | const buttonOptions = { 29 | class: 'paper-icon-button-light', 30 | is: 'paper-icon-button-light', 31 | }; 32 | const uiAnchorStr = '\uE034'; 33 | const mediaContainerQueryStr = "div[data-type='video-osd']"; 34 | const mediaQueryStr = 'video'; 35 | const displayButtonOpts = { 36 | title: '弹幕开关', 37 | id: 'displayDanmaku', 38 | innerText: null, 39 | onclick: () => { 40 | if (window.ede.loading) { 41 | console.log('正在加载,请稍后再试'); 42 | return; 43 | } 44 | console.log('切换弹幕开关'); 45 | window.ede.danmakuSwitch = (window.ede.danmakuSwitch + 1) % 2; 46 | window.localStorage.setItem('danmakuSwitch', window.ede.danmakuSwitch); 47 | document.querySelector('#displayDanmaku').children[0].innerText = danmaku_icons[window.ede.danmakuSwitch]; 48 | if (window.ede.danmaku) { 49 | window.ede.danmakuSwitch == 1 ? window.ede.danmaku.show() : window.ede.danmaku.hide(); 50 | } 51 | }, 52 | }; 53 | const searchButtonOpts = { 54 | title: '搜索弹幕', 55 | id: 'searchDanmaku', 56 | innerText: search_icon, 57 | onclick: () => { 58 | if (window.ede.loading) { 59 | console.log('正在加载,请稍后再试'); 60 | return; 61 | } 62 | console.log('手动匹配弹幕'); 63 | reloadDanmaku('search'); 64 | }, 65 | }; 66 | const translateButtonOpts = { 67 | title: null, 68 | id: 'translateDanmaku', 69 | innerText: translate_icon, 70 | onclick: () => { 71 | if (window.ede.loading) { 72 | console.log('正在加载,请稍后再试'); 73 | return; 74 | } 75 | console.log('切换简繁转换'); 76 | window.ede.chConvert = (window.ede.chConvert + 1) % 3; 77 | window.localStorage.setItem('chConvert', window.ede.chConvert); 78 | document.querySelector('#translateDanmaku').setAttribute('title', chConverTtitle[window.ede.chConvert]); 79 | reloadDanmaku('reload'); 80 | console.log(document.querySelector('#translateDanmaku').getAttribute('title')); 81 | }, 82 | }; 83 | const infoButtonOpts = { 84 | title: '弹幕信息', 85 | id: 'printDanmakuInfo', 86 | innerText: info_icon, 87 | onclick: () => { 88 | if (!window.ede.episode_info || window.ede.loading) { 89 | console.log('正在加载,请稍后再试'); 90 | return; 91 | } 92 | console.log('显示当前信息'); 93 | let msg = '动画名称:' + window.ede.episode_info.animeTitle; 94 | if (window.ede.episode_info.episodeTitle) { 95 | msg += '\n分集名称:' + window.ede.episode_info.episodeTitle; 96 | } 97 | sendNotification('当前弹幕匹配', msg); 98 | }, 99 | }; 100 | 101 | const filterButtonOpts = { 102 | title: '过滤等级(下次加载生效)', 103 | id: 'filteringDanmaku', 104 | innerText: null, 105 | onclick: () => { 106 | console.log('切换弹幕过滤等级'); 107 | let level = window.localStorage.getItem('danmakuFilterLevel'); 108 | level = ((level ? parseInt(level) : 0) + 1) % 4; 109 | window.localStorage.setItem('danmakuFilterLevel', level); 110 | document.querySelector('#filteringDanmaku').children[0].innerText = filter_icons[level]; 111 | }, 112 | }; 113 | // ------ configs end------ 114 | /* eslint-disable */ 115 | /* https://cdn.jsdelivr.net/npm/danmaku/dist/danmaku.min.js */ 116 | // prettier-ignore 117 | !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(){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=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||function(t){return setTimeout(t,50/3)},u=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 { 184 | var e_copy = e.cloneNode(true); 185 | while (e_copy.firstChild != e_copy.lastChild) { 186 | e_copy.removeChild(e_copy.lastChild); 187 | } 188 | if (e_copy.innerText.includes(innerStr)) { 189 | res.push(e); 190 | } 191 | }); 192 | return res; 193 | } 194 | 195 | function initUI() { 196 | // 页面未加载 197 | let uiAnchor = getElementsByInnerText('i', uiAnchorStr); 198 | if (!uiAnchor || !uiAnchor[0]) { 199 | return; 200 | } 201 | // 已初始化 202 | if (document.getElementById('danmakuCtr')) { 203 | return; 204 | } 205 | console.log('正在初始化UI'); 206 | // 弹幕按钮容器div 207 | let parent = uiAnchor[0].parentNode.parentNode.parentNode; 208 | let menubar = document.createElement('div'); 209 | menubar.id = 'danmakuCtr'; 210 | if (!window.ede.episode_info) { 211 | menubar.style.opacity = 0.5; 212 | } 213 | parent.append(menubar); 214 | // 弹幕开关 215 | displayButtonOpts.innerText = danmaku_icons[window.ede.danmakuSwitch]; 216 | menubar.appendChild(createButton(displayButtonOpts)); 217 | // 手动匹配 218 | menubar.appendChild(createButton(searchButtonOpts)); 219 | // 简繁转换 220 | translateButtonOpts.title = chConverTtitle[window.ede.chConvert]; 221 | menubar.appendChild(createButton(translateButtonOpts)); 222 | // 屏蔽等级 223 | filterButtonOpts.innerText = filter_icons[parseInt(window.localStorage.getItem('danmakuFilterLevel') ? window.localStorage.getItem('danmakuFilterLevel') : 0)]; 224 | menubar.appendChild(createButton(filterButtonOpts)); 225 | // 弹幕信息 226 | menubar.appendChild(createButton(infoButtonOpts)); 227 | console.log('UI初始化完成'); 228 | } 229 | 230 | function sendNotification(title, msg) { 231 | const Notification = window.Notification || window.webkitNotifications; 232 | console.log(msg); 233 | if (Notification.permission === 'granted') { 234 | return new Notification(title, { 235 | body: msg, 236 | }); 237 | } else { 238 | Notification.requestPermission((permission) => { 239 | if (permission === 'granted') { 240 | return new Notification(title, { 241 | body: msg, 242 | }); 243 | } 244 | }); 245 | } 246 | } 247 | 248 | function getEmbyItemInfo() { 249 | return window.require(['pluginManager']).then((items) => { 250 | if (items) { 251 | for (let i = 0; i < items.length; i++) { 252 | const item = items[i]; 253 | if (item.pluginsList) { 254 | for (let j = 0; j < item.pluginsList.length; j++) { 255 | const plugin = item.pluginsList[j]; 256 | if (plugin && plugin.id == 'htmlvideoplayer') { 257 | return plugin._currentPlayOptions ? plugin._currentPlayOptions.item : null; 258 | } 259 | } 260 | } 261 | } 262 | } 263 | return null; 264 | }); 265 | } 266 | 267 | async function getEpisodeInfo(is_auto = true) { 268 | let item = await getEmbyItemInfo(); 269 | if (!item) { 270 | return null; 271 | } 272 | let _id; 273 | let animeName; 274 | let anime_id = -1; 275 | let episode; 276 | if (item.Type == 'Episode') { 277 | _id = item.SeasonId; 278 | animeName = item.SeriesName; 279 | episode = item.IndexNumber; 280 | let session = item.ParentIndexNumber; 281 | if (session != 1) { 282 | animeName += ' ' + session; 283 | } 284 | } else { 285 | _id = item.Id; 286 | animeName = item.Name; 287 | episode = 'movie'; 288 | } 289 | let _id_key = '_anime_id_rel_' + _id; 290 | let _name_key = '_anime_name_rel_' + _id; 291 | let _episode_key = '_episode_id_rel_' + _id + '_' + episode; 292 | if (is_auto) { 293 | if (window.localStorage.getItem(_episode_key)) { 294 | return JSON.parse(window.localStorage.getItem(_episode_key)); 295 | } 296 | } 297 | if (window.localStorage.getItem(_id_key)) { 298 | anime_id = window.localStorage.getItem(_id_key); 299 | } 300 | if (window.localStorage.getItem(_name_key)) { 301 | animeName = window.localStorage.getItem(_name_key); 302 | } 303 | if (!is_auto) { 304 | animeName = prompt('确认动画名:', animeName); 305 | } 306 | 307 | let searchUrl = 'https://api.dandanplay.net/api/v2/search/episodes?anime=' + animeName + '&withRelated=true'; 308 | if (is_auto) { 309 | searchUrl += '&episode=' + episode; 310 | } 311 | let animaInfo = await fetch(searchUrl, { 312 | method: 'GET', 313 | headers: { 314 | 'Accept-Encoding': 'gzip', 315 | Accept: 'application/json', 316 | 'User-Agent': navigator.userAgent, 317 | }, 318 | }) 319 | .then((response) => response.json()) 320 | .catch((error) => { 321 | console.log('查询失败:', error); 322 | return null; 323 | }); 324 | console.log('查询成功'); 325 | console.log(animaInfo); 326 | let selecAnime_id = 1; 327 | if (anime_id != -1) { 328 | for (let index = 0; index < animaInfo.animes.length; index++) { 329 | if (animaInfo.animes[index].animeId == anime_id) { 330 | selecAnime_id = index + 1; 331 | } 332 | } 333 | } 334 | if (!is_auto) { 335 | let anime_lists_str = list2string(animaInfo); 336 | console.log(anime_lists_str); 337 | selecAnime_id = prompt('选择:\n' + anime_lists_str, selecAnime_id); 338 | selecAnime_id = parseInt(selecAnime_id) - 1; 339 | window.localStorage.setItem(_id_key, animaInfo.animes[selecAnime_id].animeId); 340 | window.localStorage.setItem(_name_key, animaInfo.animes[selecAnime_id].animeTitle); 341 | let episode_lists_str = ep2string(animaInfo.animes[selecAnime_id].episodes); 342 | episode = prompt('确认集数:\n' + episode_lists_str, parseInt(episode)); 343 | episode = parseInt(episode) - 1; 344 | } else { 345 | selecAnime_id = parseInt(selecAnime_id) - 1; 346 | episode = 0; 347 | } 348 | let episodeInfo = { 349 | episodeId: animaInfo.animes[selecAnime_id].episodes[episode].episodeId, 350 | animeTitle: animaInfo.animes[selecAnime_id].animeTitle, 351 | episodeTitle: animaInfo.animes[selecAnime_id].type == 'tvseries' ? animaInfo.animes[selecAnime_id].episodes[episode].episodeTitle : null, 352 | }; 353 | window.localStorage.setItem(_episode_key, JSON.stringify(episodeInfo)); 354 | return episodeInfo; 355 | } 356 | 357 | function getComments(episodeId) { 358 | let url = 'https://api.9-ch.com/cors/https://api.dandanplay.net/api/v2/comment/' + episodeId + '?withRelated=true&chConvert=' + window.ede.chConvert; 359 | return fetch(url, { 360 | method: 'GET', 361 | headers: { 362 | 'Accept-Encoding': 'gzip', 363 | Accept: 'application/json', 364 | 'User-Agent': navigator.userAgent, 365 | }, 366 | }) 367 | .then((response) => response.json()) 368 | .then((data) => { 369 | console.log('弹幕下载成功: ' + data.comments.length); 370 | return data.comments; 371 | }) 372 | .catch((error) => { 373 | console.log('获取弹幕失败:', error); 374 | return null; 375 | }); 376 | } 377 | 378 | async function createDanmaku(comments) { 379 | if (!comments) { 380 | return; 381 | } 382 | if (window.ede.danmaku != null) { 383 | window.ede.danmaku.clear(); 384 | window.ede.danmaku.destroy(); 385 | window.ede.danmaku = null; 386 | } 387 | let _comments = danmakuFilter(danmakuParser(comments)); 388 | console.log('弹幕加载成功: ' + _comments.length); 389 | 390 | while (!document.querySelector(mediaContainerQueryStr)) { 391 | await new Promise((resolve) => setTimeout(resolve, 200)); 392 | } 393 | 394 | let _container = document.querySelector(mediaContainerQueryStr); 395 | let _media = document.querySelector(mediaQueryStr); 396 | window.ede.danmaku = new Danmaku({ 397 | container: _container, 398 | media: _media, 399 | comments: _comments, 400 | engine: 'canvas', 401 | }); 402 | window.ede.danmakuSwitch == 1 ? window.ede.danmaku.show() : window.ede.danmaku.hide(); 403 | if (window.ede.ob) { 404 | window.ede.ob.disconnect(); 405 | } 406 | window.ede.ob = new ResizeObserver(() => { 407 | if (window.ede.danmaku) { 408 | console.log('Resizing'); 409 | window.ede.danmaku.resize(); 410 | } 411 | }); 412 | window.ede.ob.observe(_container); 413 | } 414 | 415 | function reloadDanmaku(type = 'check') { 416 | if (window.ede.loading) { 417 | console.log('正在重新加载'); 418 | return; 419 | } 420 | window.ede.loading = true; 421 | getEpisodeInfo(type != 'search') 422 | .then((info) => { 423 | return new Promise((resolve, reject) => { 424 | if (!info) { 425 | if (type != 'init') { 426 | reject('播放器未完成加载'); 427 | } else { 428 | reject(null); 429 | } 430 | } 431 | if (type != 'search' && type != 'reload' && window.ede.danmaku && window.ede.episode_info && window.ede.episode_info.episodeId == info.episodeId) { 432 | reject('当前播放视频未变动'); 433 | } else { 434 | window.ede.episode_info = info; 435 | resolve(info.episodeId); 436 | } 437 | }); 438 | }) 439 | .then( 440 | (episodeId) => 441 | getComments(episodeId).then((comments) => 442 | createDanmaku(comments).then(() => { 443 | console.log('弹幕就位'); 444 | }), 445 | ), 446 | (msg) => { 447 | if (msg) { 448 | console.log(msg); 449 | } 450 | }, 451 | ) 452 | .then(() => { 453 | window.ede.loading = false; 454 | if (document.getElementById('danmakuCtr').style.opacity != 1) { 455 | document.getElementById('danmakuCtr').style.opacity = 1; 456 | } 457 | }); 458 | } 459 | 460 | function danmakuFilter(comments) { 461 | let level = parseInt(window.localStorage.getItem('danmakuFilterLevel') ? window.localStorage.getItem('danmakuFilterLevel') : 0); 462 | if (level == 0) { 463 | return comments; 464 | } 465 | let limit = 9 - level * 2; 466 | let vertical_limit = 6; 467 | let arr_comments = []; 468 | let vertical_comments = []; 469 | for (let index = 0; index < comments.length; index++) { 470 | let element = comments[index]; 471 | let i = Math.ceil(element.time); 472 | let i_v = Math.ceil(element.time / 3); 473 | if (!arr_comments[i]) { 474 | arr_comments[i] = []; 475 | } 476 | if (!vertical_comments[i_v]) { 477 | vertical_comments[i_v] = []; 478 | } 479 | // TODO: 屏蔽过滤 480 | if (vertical_comments[i_v].length < vertical_limit) { 481 | vertical_comments[i_v].push(element); 482 | } else { 483 | element.mode = 'rtl'; 484 | } 485 | if (arr_comments[i].length < limit) { 486 | arr_comments[i].push(element); 487 | } 488 | } 489 | return arr_comments.flat(); 490 | } 491 | 492 | function danmakuParser($obj) { 493 | //const $xml = new DOMParser().parseFromString(string, 'text/xml') 494 | return $obj 495 | .map(($comment) => { 496 | const p = $comment.p; 497 | //if (p === null || $comment.childNodes[0] === undefined) return null 498 | const values = p.split(','); 499 | const mode = { 6: 'ltr', 1: 'rtl', 5: 'top', 4: 'bottom' }[values[1]]; 500 | if (!mode) return null; 501 | //const fontSize = Number(values[2]) || 25 502 | const fontSize = Math.round((window.screen.height > window.screen.width ? window.screen.width : window.screen.height / 1080) * 18); 503 | const color = `000000${Number(values[2]).toString(16)}`.slice(-6); 504 | return { 505 | text: $comment.m, 506 | mode, 507 | time: values[0] * 1, 508 | style: { 509 | fontSize: `${fontSize}px`, 510 | color: `#${color}`, 511 | textShadow: 512 | 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', 513 | 514 | font: `${fontSize}px sans-serif`, 515 | fillStyle: `#${color}`, 516 | strokeStyle: color === '000000' ? '#fff' : '#000', 517 | lineWidth: 2.0, 518 | }, 519 | }; 520 | }) 521 | .filter((x) => x); 522 | } 523 | 524 | function list2string($obj2) { 525 | const $animes = $obj2.animes; 526 | let anime_lists = $animes.map(($single_anime) => { 527 | return $single_anime.animeTitle + ' 类型:' + $single_anime.typeDescription; 528 | }); 529 | let anime_lists_str = '1:' + anime_lists[0]; 530 | for (let i = 1; i < anime_lists.length; i++) { 531 | anime_lists_str = anime_lists_str + '\n' + (i + 1).toString() + ':' + anime_lists[i]; 532 | } 533 | return anime_lists_str; 534 | } 535 | 536 | function ep2string($obj3) { 537 | const $animes = $obj3; 538 | let anime_lists = $animes.map(($single_ep) => { 539 | return $single_ep.episodeTitle; 540 | }); 541 | let ep_lists_str = '1:' + anime_lists[0]; 542 | for (let i = 1; i < anime_lists.length; i++) { 543 | ep_lists_str = ep_lists_str + '\n' + (i + 1).toString() + ':' + anime_lists[i]; 544 | } 545 | return ep_lists_str; 546 | } 547 | 548 | while (!window.require) { 549 | await new Promise((resolve) => setTimeout(resolve, 200)); 550 | } 551 | if (!window.ede) { 552 | window.ede = new EDE(); 553 | setInterval(() => { 554 | initUI(); 555 | }, check_interval); 556 | while (!(await getEmbyItemInfo())) { 557 | await new Promise((resolve) => setTimeout(resolve, 200)); 558 | } 559 | reloadDanmaku('init'); 560 | setInterval(() => { 561 | initListener(); 562 | }, check_interval); 563 | } 564 | } 565 | })(); 566 | --------------------------------------------------------------------------------