/* eslint-disable indent */ /* eslint-disable max-len */ /* eslint-disable no-undef */ // ==UserScript== // @name SakuraDanmaku 樱花弹幕 // @namespace https://muted.top/ // @version 1.0.3 // @description yhdm, but with Danmaku from Bilibili 让樱花动漫和橘子动漫加载 Bilibili 弹幕 // @author MUTED64 // @match *://*.yhpdm.net/vp/* // @match *://*.mgnacg.com/bangumi/* // @match *://*.akkdm.com/play/* // @match *://*.yinghuacd.com/v/* // @match https://www.yhpdm.net/yxsf/player/dpx2/* // @match https://play.mknacg.top:8585/* // @match https://www.akkdm.com/dp/* // @match https://tup.yinghuacd.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addElement // @grant GM_addStyle // @connect api.bilibili.com // @icon https://www.yhdmp.cc/yxsf/yh_pic/favicon.ico // @require https://bowercdn.net/c/danmaku-2.0.4/dist/danmaku.dom.min.js // @license GPLv3 // @run-at document-end // @downloadURL none // ==/UserScript== "use strict"; const sites = { yhdm: { address: /.*:\/\/.*\.yhpdm\.net\/vp\/.*/, videoFrame: "iframe", videoFrameURL: "https://www.yhpdm.net/yxsf/player/dpx2", bangumiTitle: "title", episode: "div.gohome > span", container: "div.dplayer-video-wrap", video: "div.dplayer-video-wrap > video", iconsBar: "div.dplayer-controller > div.dplayer-icons.dplayer-icons-right", panelLeft: "1em", panelTop: "42%", panelTransform: "translateY(-50%)", }, mgnacg: { address: /.*:\/\/.*\.mgnacg\.com\/bangumi\/.*/, videoFrame: "iframe#videoiframe", videoFrameURL: "https://play.mknacg.top:8585", bangumiTitle: "h1.page-title > a", episode: "span.btn-pc.page-title", container: "div.art-video-player", video: "div.art-video-player > video.art-video", iconsBar: "div.art-video-player div.art-controls > div.art-controls-right", panelRight: "10em", panelBottom: "2%", }, akkdm: { address: /.*:\/\/.*\.akkdm\.com\/play\/.*/, videoFrame: "#playleft > iframe", videoFrameURL: "https://www.akkdm.com/dp", bangumiTitle: "body > div.page.player > div.main > div > div.module.module-player > div > div.module-player-side > div.module-player-info > div > h1 > a", episode: "#panel2 > div > div > a.module-play-list-link.active", container: "div.video-wrapper", video: "div.video-wrapper > video", iconsBar: "div.art-video-player div.art-controls > div.art-controls-right", panelLeft: "1px", panelBottom: "2%", }, yinghuacd: { address: /.*:\/\/.*\.yinghuacd\.com\/v\/.*/, videoFrame: "iframe", videoFrameURL: "https://tup.yinghuacd.com", bangumiTitle: "div.gohome > h1 > a", episode: "div.gohome span", container: "div.dplayer-video-wrap", video: "div.dplayer-video-wrap > video", iconsBar: "div.dplayer-controller > div.dplayer-icons.dplayer-icons-right", panelLeft: "1em", panelTop: "42%", panelTransform: "translateY(-50%)", }, }; class BilibiliDanmaku { static #EP_API_BASE = "https://api.bilibili.com/pgc/view/web/season"; static #DANMAKU_API_BASE = "https://api.bilibili.com/x/v1/dm/list.so"; static #KEYWORD_API_BASE = "https://api.bilibili.com/x/web-interface/search/type?search_type=media_bangumi"; constructor(keyword, episode) { this.keyword = keyword; this.episode = episode; } // GM_xmlhttpRequest的Promise封装 #Get(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: (response) => { resolve(response.responseText); }, onerror: (error) => { reject(error); }, }); }); } // Bilibili弹幕xml串转换为可加载的对象 #parseBilibiliDanmaku(string) { const $xml = new DOMParser().parseFromString(string, "text/xml"); return [...$xml.getElementsByTagName("d")] .map(($d) => { const p = $d.getAttribute("p"); if (p === null || $d.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 color = `000000${Number(values[3]).toString(16)}`.slice(-6); return { text: $d.childNodes[0].nodeValue, mode, time: values[0] * 1, baseTime: values[0] * 1, style: { fontSize: `${fontSize}px`, color: `#${color}`, textShadow: "0px 1px 3px #000,0px 0px 3px #000", font: `${fontSize}px sans-serif`, fillStyle: `#${color}`, strokeStyle: color === "000000" ? "#fff" : "#000", lineWidth: 2.0, }, }; }) .filter((x) => x); } // 获取Bilibili对应视频的弹幕 async getInfoAndDanmaku(xml = undefined) { if (!xml) { const fetchedFromKeyword = JSON.parse( await this.#Get( `${this.constructor.#KEYWORD_API_BASE}&keyword=${this.keyword}` ) ).data.result; this.mdid = fetchedFromKeyword[0].media_id; this.ssid = fetchedFromKeyword[0].season_id; this.epid = fetchedFromKeyword[0].eps[0].id; // 获取cid let { code, message, result } = JSON.parse( await this.#Get(`${this.constructor.#EP_API_BASE}?ep_id=${this.epid}`) ); if (code) { throw new Error(message); } this.cid = result.episodes[this.episode - 1].cid; // 获取弹幕 this.danmaku = this.#parseBilibiliDanmaku( await this.#Get(`${this.constructor.#DANMAKU_API_BASE}?oid=${this.cid}`) ); this.basic_info = { mdid: this.mdid, ssid: this.ssid, epid: this.epid, cid: this.cid, danmaku: this.danmaku, }; return this.basic_info; } else { this.basic_info = { danmaku: this.#parseBilibiliDanmaku(xml) }; return this.basic_info; } } } class DanmakuControl { danmaku; danmakuElement; constructor(keyword, episode, container, video) { this.keyword = keyword; this.episode = episode; this.container = document.querySelector(container); this.video = document.querySelector(video); const selectInterval = setInterval(() => { this.container = document.querySelector(container); this.video = document.querySelector(video); if (this.container && this.video) { clearInterval(selectInterval); } }, 500); } async load(xml = undefined) { const bilibiliDanmaku = new BilibiliDanmaku(this.keyword, this.episode); this.basic_info = await bilibiliDanmaku.getInfoAndDanmaku(xml); const loadInterval = setInterval(() => { if (this.container && this.video) { this.danmaku = new Danmaku({ container: this.container, media: this.video, comments: this.basic_info.danmaku, speed: 144, }); clearInterval(loadInterval); } }, 500); } show() { const showInterval = setInterval(() => { if (this.container && this.video) { this.video.style.position = "absolute"; this.danmaku.show(); this.danmakuElement = this.container.lastElementChild; this.danmakuElement.style.zIndex = 1000; this.danmakuSettings = getStoredSettings(); this.applySettings(this.danmakuSettings); let resizeObserver = new ResizeObserver(() => { this.danmaku.resize(); }); resizeObserver.observe(this.container); clearInterval(showInterval); } }, 500); } toggleShowAndHide(show) { if (show) { this.danmakuElement.style.display = "block"; } else { this.danmakuElement.style.display = "none"; } } destroy() { this.danmaku.destroy(); } setSpeed(speed) { this.danmaku.speed = Number(speed); } setFontSize(fontSize) { for (const i of this.danmaku.comments) { i.style.font = `${fontSize}px sans-serif`; } } setLimit(percentLimit) { for (const i of this.danmaku.comments) { i.style.display = "block"; if (Math.random() > percentLimit) { i.style.display = "none"; } } } setOpacity(opacity) { this.danmakuElement.style.opacity = opacity; } setOffset(offset) { for (const comment of this.danmaku.comments) { comment.time = comment.baseTime - Number(offset); } this.video.currentTime = Number(this.video.currentTime); } applySettings(settings) { this.toggleShowAndHide(settings.show); this.setSpeed(settings.speed); this.setOpacity(settings.opacity); this.setFontSize(settings.fontSize); this.setLimit(settings.limit); } } function getKeywordAndEpisode(currentSite) { const keyword = document .querySelector(currentSite.bangumiTitle) .textContent.replace(/ 第[0-9]+集.*/gi, "") .replace(/ 第[0-9]+话.*/gi, "") .replace(/ Part ?[0-9]+.*/, ""); const episode = Number( document .querySelector(currentSite.episode) .textContent.replace(/[^0-9]+/gi, "") ); return { keyword, episode, }; } function loadConfigToIframe( videoFrame, keyword, episode, currentSite, xml = undefined ) { videoFrame.contentWindow.postMessage( { keyword, episode, currentSite, xml, }, currentSite.videoFrameURL ); } function showChoosePanel(message, keyword, episode, currentSite, videoFrame) { if (document.querySelector(".danmakuChoose")) { document.querySelector(".danmakuChoose").remove(); } const storedSettings = getStoredSettings(); GM_addElement(document.body, "div", { class: "danmakuChoose" }); document.querySelector( ".danmakuChoose" ).innerHTML = `
${message}
或手动上传XML弹幕文件