// ==UserScript==
// @name Emby danmaku extension Reload
// @description Emby弹幕插件
// @namespace https://github.com/RyoLee
// @author RyoLee,SummerTail
// @version 1.12
// @copyright 2022, RyoLee (https://github.com/RyoLee)
// @license MIT; https://raw.githubusercontent.com/RyoLee/emby-danmaku/master/LICENSE
// @icon https://github.githubassets.com/pinned-octocat.svg
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @match */web/index.html
// ==/UserScript==
(async function () {
"use strict";
//注册插件菜单
GM_registerMenuCommand("Emby服务器名称", function () {
const EmbyName = prompt("请输入服务器名称:");
GM_setValue("EmbyName", EmbyName);
});
("use strict");
if (
document.querySelector('meta[name="application-name"]').content ==
GM_getValue("EmbyName", "Emby")
) {
const DanmaStatu = 1;
if (DanmaStatu == 0) {
return null;
}
const check_interval = 200;
const chConverTtitle = [
"当前状态: 未启用",
"当前状态: 转换为简体",
"当前状态: 转换为繁体",
];
// 0:当前状态关闭 1:当前状态打开
const danmaku_icons = ["\uE0B9", "\uE7A2"];
const search_icon = "\uE881";
const translate_icon = "\uE927";
const info_icon = "\uE0E0";
const fontSize_icon =
'<svg t="1707762095726" class="icon" viewBox="0 0 1064 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3633" width="24" height="24"><path d="M495.73888 195.91168a30.72 30.72 0 0 0-55.37792 2.2528L139.22304 900.79232a51.2 51.2 0 0 1-94.12608-40.30464L346.23488 157.81888c44.07296-102.8096 187.63776-108.70784 240.0256-9.8304l104.12032 196.68992a51.2 51.2 0 1 1-90.5216 47.9232l-104.12032-196.68992zM779.75552 570.28608c-10.36288-26.46016-47.9232-25.84576-57.50784 0.86016l-131.31776 367.65696a51.2 51.2 0 0 1-96.41984-34.4064l131.31776-367.65696c41.3696-115.87584 204.26752-118.3744 249.2416-3.93216l145.408 370.0736a51.2 51.2 0 1 1-95.31392 37.43744l-145.408-370.03264z" fill="#ffffff" p-id="3634" data-spm-anchor-id="a313x.search_index.0.i12.73ca3a81uN587a" class="selected"></path><path d="M163.84 593.92c0-28.2624 22.9376-51.2 51.2-51.2h266.24a51.2 51.2 0 1 1 0 102.4h-266.24c-28.2624 0-51.2-22.9376-51.2-51.2zM573.44 778.24c0-28.2624 22.9376-51.2 51.2-51.2h266.24a51.2 51.2 0 1 1 0 102.4h-266.24c-28.2624 0-51.2-22.9376-51.2-51.2z" fill="#ffffff" p-id="3635" data-spm-anchor-id="a313x.search_index.0.i13.73ca3a81uN587a" class="selected"></path></svg>';
const danmaku_Url_icon =
'<svg t="1707798572282" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4558" width="24" height="24"><path d="M317.226667 382.122667c17.92 17.92 17.92 47.018667 0 64.938666L252.288 512l-6.314667 6.656a183.68 183.68 0 0 0-0.213333 246.186667l6.528 6.869333 6.613333 6.314667a183.68 183.68 0 0 0 246.229334 0.213333l6.869333-6.528 64.938667-64.938667a45.909333 45.909333 0 0 1 64.938666 64.938667l-64.938666 64.938667a275.456 275.456 0 0 1-396.757334-382.122667l7.168-7.466667 64.938667-64.938666c17.92-17.92 46.976-17.92 64.938667 0z m354.816-34.730667l4.565333 4.565333a42.666667 42.666667 0 0 1 0 60.373334l-264.277333 264.277333a42.666667 42.666667 0 0 1-60.373334 0l-4.565333-4.565333a42.666667 42.666667 0 0 1 0-60.373334l264.277333-264.277333a42.666667 42.666667 0 0 1 60.373334 0z m164.608-160.042667a275.498667 275.498667 0 0 1 7.168 382.122667l-7.168 7.466667-64.938667 64.938666a45.909333 45.909333 0 0 1-64.938667-64.938666L771.712 512l6.314667-6.656a183.68 183.68 0 0 0 0.213333-246.186667l-6.528-6.869333-6.613333-6.314667a183.68 183.68 0 0 0-246.229334-0.213333L512 252.288l-64.938667 64.938667a45.909333 45.909333 0 0 1-64.938666-64.938667l64.938666-64.938667a275.456 275.456 0 0 1 389.589334 0z" fill="#ffffff" p-id="4559" data-spm-anchor-id="a313x.search_index.0.i6.29243a81qdtCEA" class="selected"></path></svg>';
const filter_icons = ["\uE3E0", "\uE3D0", "\uE3D1", "\uE3D2"];
const buttonOptions = {
class: "paper-icon-button-light",
is: "paper-icon-button-light",
};
const uiAnchorStr = "\uE034";
const mediaContainerQueryStr = "body > div.view-videoosd-videoosd";
const mediaQueryStr = "video";
const fontSize_Setting = parseInt(
window.localStorage.getItem("danmakuFontSize")
? window.localStorage.getItem("danmakuFontSize")
: 18
);
const displayButtonOpts = {
title: "弹幕开关",
id: "displayDanmaku",
innerText: null,
onclick: () => {
if (window.ede.loading) {
console.log("正在加载,请稍后再试");
return;
}
console.log("切换弹幕开关");
window.ede.danmakuSwitch = (window.ede.danmakuSwitch + 1) % 2;
window.localStorage.setItem(
"danmakuSwitch",
window.ede.danmakuSwitch
);
document.querySelector(
"#displayDanmaku"
).children[0].innerText =
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,
onclick: () => {
if (window.ede.loading) {
console.log("正在加载,请稍后再试");
return;
}
console.log("手动匹配弹幕");
reloadDanmaku("search");
},
};
const getUrlDanmaku = {
title: "下载字幕",
id: "getUrlDanmaku",
innerText: danmaku_Url_icon,
onclick: () => {
if (window.ede.loading) {
console.log("正在加载,请稍后再试");
return;
}
console.log("手动匹配下载弹幕");
reloadDanmakuUrl();
},
};
const translateButtonOpts = {
title: null,
id: "translateDanmaku",
innerText: translate_icon,
onclick: () => {
if (window.ede.loading) {
console.log("正在加载,请稍后再试");
return;
}
console.log("切换简繁转换");
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");
console.log(
document
.querySelector("#translateDanmaku")
.getAttribute("title")
);
},
};
const infoButtonOpts = {
title: "弹幕信息",
id: "printDanmakuInfo",
innerText: info_icon,
onclick: () => {
if (!window.ede.episode_info || window.ede.loading) {
console.log("正在加载,请稍后再试");
return;
}
console.log("显示当前信息");
let msg = "动画名称:" + window.ede.episode_info.animeTitle;
if (window.ede.episode_info.episodeTitle) {
msg += "\n分集名称:" + window.ede.episode_info.episodeTitle;
}
sendNotification("当前弹幕匹配", msg);
},
};
const fontSizeSetting = {
title:
"字体大小(下次加载生效:" +
window.localStorage.getItem("danmakuFontSize") +
")",
id: "fontSizeSetting",
innerText: fontSize_icon,
onclick: () => {
console.log("修改弹幕字体大小");
const size = parseInt(prompt("请输入字体大小:"));
if (0 < size) {
window.localStorage.setItem("danmakuFontSize", size);
} else {
alert("请输入正确的字体大小");
}
},
};
const filterButtonOpts = {
title: "过滤等级(下次加载生效)",
id: "filteringDanmaku",
innerText: null,
onclick: () => {
console.log("切换弹幕过滤等级");
let level = window.localStorage.getItem("danmakuFilterLevel");
level = ((level ? parseInt(level) : 0) + 1) % 4;
window.localStorage.setItem("danmakuFilterLevel", level);
document.querySelector(
"#filteringDanmaku"
).children[0].innerText = filter_icons[level];
},
};
// ------ configs end------
/* eslint-disable */
/* https://cdn.jsdelivr.net/npm/danmaku/dist/danmaku.min.js */
// prettier-ignore
/*
/* eslint-enable */
class EDE {
constructor() {
this.chConvert = 1;
if (window.localStorage.getItem('chConvert')) {
this.chConvert = window.localStorage.getItem('chConvert');
}
// 0:当前状态关闭 1:当前状态打开
this.danmakuSwitch = 1;
if (window.localStorage.getItem('danmakuSwitch')) {
this.danmakuSwitch = parseInt(window.localStorage.getItem('danmakuSwitch'));
}
this.danmaku = null;
this.episode_info = null;
this.ob = null;
this.loading = false;
}
}
function createButton(opt) {
let button = document.createElement("button", buttonOptions);
button.setAttribute("title", opt.title);
button.setAttribute("id", opt.id);
let icon = document.createElement("span");
icon.className = "md-icon";
icon.innerHTML = opt.innerText;
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;
}
if (!container.getAttribute("ede_listening")) {
console.log("正在初始化Listener");
container.setAttribute("ede_listening", true);
container.addEventListener("play", reloadDanmaku);
console.log("Listener初始化完成");
reloadDanmaku("reload");
}
}
function getElementsByInnerText(
tagType,
innerStr,
excludeChildNode = true
) {
var temp = [];
var elements = document.getElementsByTagName(tagType);
if (!elements || 0 == elements.length) {
return temp;
}
for (let index = 0; index < elements.length; index++) {
var e = elements[index];
if (e.innerText.includes(innerStr)) {
temp.push(e);
}
}
if (!excludeChildNode) {
return temp;
}
var res = [];
temp.forEach((e) => {
var e_copy = e.cloneNode(true);
while (e_copy.firstChild != e_copy.lastChild) {
e_copy.removeChild(e_copy.lastChild);
}
if (e_copy.innerText.includes(innerStr)) {
res.push(e);
}
});
return res;
}
function initUI() {
// 页面未加载
let uiAnchor = getElementsByInnerText("i", uiAnchorStr);
if (!uiAnchor || !uiAnchor[0]) {
return;
}
// 已初始化
if (document.getElementById("danmakuCtr")) {
return;
}
console.log("正在初始化UI");
// 弹幕按钮容器div
let parent = uiAnchor[0].parentNode.parentNode.parentNode;
let menubar = document.createElement("div");
menubar.id = "danmakuCtr";
if (!window.ede.episode_info) {
menubar.style.opacity = 0.5;
}
parent.append(menubar);
// 弹幕开关
displayButtonOpts.innerText =
danmaku_icons[window.ede.danmakuSwitch];
menubar.appendChild(createButton(displayButtonOpts));
//下载字幕
menubar.appendChild(createButton(getUrlDanmaku));
// 手动匹配
menubar.appendChild(createButton(searchButtonOpts));
// 简繁转换
translateButtonOpts.title = chConverTtitle[window.ede.chConvert];
menubar.appendChild(createButton(translateButtonOpts));
// 屏蔽等级
filterButtonOpts.innerText =
filter_icons[
parseInt(
window.localStorage.getItem("danmakuFilterLevel")
? window.localStorage.getItem("danmakuFilterLevel")
: 0
)
];
menubar.appendChild(createButton(filterButtonOpts));
// 弹幕信息
menubar.appendChild(createButton(infoButtonOpts));
//字体大小
menubar.appendChild(createButton(fontSizeSetting));
console.log("UI初始化完成");
}
function sendNotification(title, msg) {
const Notification =
window.Notification || window.webkitNotifications;
console.log(msg);
if (Notification.permission === "granted") {
return new Notification(title, {
body: msg,
});
} else {
Notification.requestPermission((permission) => {
if (permission === "granted") {
return new Notification(title, {
body: msg,
});
}
});
}
}
function getEmbyItemInfo() {
if (location.hash != "#!/videoosd/videoosd.html") {
return null;
}
return window.require(["pluginManager"]).then((items) => {
if (items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.pluginsList) {
for (let j = 0; j < item.pluginsList.length; j++) {
const plugin = item.pluginsList[j];
if (plugin && plugin.id == "htmlvideoplayer") {
return plugin._currentPlayOptions
? plugin._currentPlayOptions.item
: null;
}
}
}
}
}
return null;
});
}
async function getEpisodeInfo(is_auto = true) {
let item = await getEmbyItemInfo();
if (!item) {
return null;
}
if (item.Type == "TvChannel") {
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 =
"https://api.9-ch.com/cors/https://api.dandanplay.net/api/v2/search/episodes?anime=" +
animeName +
"&withRelated=true";
if (is_auto) {
searchUrl += "&episode=" + episode;
}
let animaInfo = await fetch(searchUrl, {
method: "GET",
headers: {
"Accept-Encoding": "gzip",
Accept: "application/json",
"User-Agent": navigator.userAgent,
},
})
.then((response) => response.json())
.catch((error) => {
console.log("查询失败:", error);
return null;
});
console.log("查询成功");
console.log(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);
console.log(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 =
"https://api.9-ch.com/cors/https://api.dandanplay.net/api/v2/comment/" +
episodeId +
"?withRelated=true&chConvert=" +
window.ede.chConvert;
return fetch(url, {
method: "GET",
headers: {
"Accept-Encoding": "gzip",
Accept: "application/json",
"User-Agent": navigator.userAgent,
},
})
.then((response) => response.json())
.then((data) => {
console.log("弹幕下载成功: " + data.comments.length);
return data.comments;
})
.catch((error) => {
console.log("获取弹幕失败:", error);
return null;
});
}
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));
console.log("弹幕加载成功: " + _comments.length);
while (!document.querySelector(mediaContainerQueryStr)) {
await new Promise((resolve) => setTimeout(resolve, 200));
}
let _container = document.querySelector(mediaContainerQueryStr);
let _media = document.querySelector(mediaQueryStr);
window.ede.danmaku = new Danmaku({
container: _container,
media: _media,
comments: _comments,
engine: "canvas",
});
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) {
console.log("Resizing");
window.ede.danmaku.resize();
}
});
window.ede.ob.observe(_container);
}
function reloadDanmaku(type = "check") {
if (window.ede.loading) {
console.log("正在重新加载");
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(() => {
console.log("弹幕就位");
})
),
(msg) => {
if (msg) {
console.log(msg);
}
}
)
.then(() => {
window.ede.loading = false;
if (document.getElementById("danmakuCtr")) {
if (
document.getElementById("danmakuCtr").style
.opacity != 1
) {
document.getElementById(
"danmakuCtr"
).style.opacity = 1;
}
}
});
}
function reloadDanmakuUrl() {
if (window.ede.loading) {
console.log("正在重新加载");
return;
}
window.ede.loading = true;
getDownloadDanmaku()
.then((comments) =>
createDanmaku(comments).then(() => {
console.log("弹幕就位");
})
)
.then(() => {
window.ede.loading = false;
if (document.getElementById("danmakuCtr")) {
if (
document.getElementById("danmakuCtr").style
.opacity != 1
) {
document.getElementById(
"danmakuCtr"
).style.opacity = 1;
}
}
});
}
function getDownloadDanmaku() {
const UrlPath = prompt("请输入视频站播放地址");
let url =
"https://api.9-ch.com/cors/https://api.dandanplay.net/api/v2/extcomment?url=" +
UrlPath;
return fetch(url, {
method: "GET",
headers: {
"Accept-Encoding": "gzip",
Accept: "application/json",
"User-Agent": navigator.userAgent,
},
})
.then((response) => response.json())
.then((data) => {
console.log("弹幕下载成功: " + data.comments.length);
return data.comments;
})
.catch((error) => {
console.log("获取弹幕失败:", error);
return null;
});
}
function danmakuFilter(comments) {
let level =
3 -
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')
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) * fontSize_Setting
);
const color = `000000${Number(values[2]).toString(
16
)}`.slice(-6);
return {
text: $comment.m,
mode,
time: values[0] * 1,
style: {
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",
font: `${fontSize}px sans-serif`,
fillStyle: `#${color}`,
strokeStyle: color === "000000" ? "#fff" : "#000",
lineWidth: 2.0,
},
};
})
.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 (!window.require) {
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));
//}
//if (location.hash == '#!/videoosd/videoosd.html') {
reloadDanmaku("init");
setInterval(() => {
initListener();
}, check_interval);
//}
}
//}
}
})();