您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动匹配加载动漫剧集对应弹幕并播放,目前支持樱花动漫、风车动漫
当前为
// ==UserScript== // @name 动漫网站弹幕播放 // @namespace https://github.com/LesslsMore/yhdm-danmu-player-ts // @version 0.3.4 // @author lesslsmore // @description 自动匹配加载动漫剧集对应弹幕并播放,目前支持樱花动漫、风车动漫 // @license MIT // @include /^https:\/\/www\.dmla.*\.com\/play\/.*$/ // @include https://www.tt776b.com/play/* // @include https://www.dm539.com/play/* // @require https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/artplayer.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/artplayer-plugin-danmuku.js // @connect https://api.dandanplay.net/* // @connect https://danmu.yhdmjx.com/* // @connect http://v16m-default.akamaized.net/* // @connect self // @connect * // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @run-at document-end // ==/UserScript== (async function (CryptoJS, artplayerPluginDanmuku, Artplayer) { 'use strict'; (function() { var originalSetItem = localStorage.setItem; var originalRemoveItem = localStorage.removeItem; localStorage.setItem = function(key2, value) { var event = new Event("itemInserted"); event.key = key2; event.value = value; document.dispatchEvent(event); originalSetItem.apply(this, arguments); }; localStorage.removeItem = function(key2) { var event = new Event("itemRemoved"); event.key = key2; document.dispatchEvent(event); originalRemoveItem.apply(this, arguments); }; })(); function get_anime_info(url2) { let episode2 = url2.split("-").pop().split(".")[0]; let include = [ /^https:\/\/www\.dmla.*\.com\/play\/.*$/, // 风车动漫 "https://www.tt776b.com/play/*", // 风车动漫 "https://www.dm539.com/play/*" // 樱花动漫 ]; let els = [ document.querySelector(".stui-player__detail.detail > h1 > a"), document.querySelector("body > div.myui-player.clearfix > div > div > div.myui-player__data.hidden-xs.clearfix > h3 > a"), document.querySelector(".myui-panel__head.active.clearfix > h3 > a") ]; let el; let title2; for (let i = 0; i < include.length; i++) { if (url2.match(include[i])) { el = els[i]; } } if (el != void 0) { title2 = el.text; } else { title2 = ""; console.log("没有自动匹配到动漫名称"); } return { episode: episode2, title: title2 }; } var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); function xhr_get(url2) { return new Promise((resolve, reject) => { _GM_xmlhttpRequest({ url: url2, method: "GET", headers: {}, onload: function(xhr) { resolve(xhr.responseText); } }); }); } function request(opts) { let { url: url2, method, params } = opts; if (params) { let u = new URL(url2); Object.keys(params).forEach((key2) => { const value = params[key2]; if (value !== void 0 && value !== null) { u.searchParams.set(key2, params[key2]); } }); url2 = u.toString(); } console.log("请求地址: ", url2); return new Promise((resolve, reject) => { _GM_xmlhttpRequest({ url: url2, method: method || "GET", responseType: "json", onload: (res) => { resolve(res.response); }, onerror: reject }); }); } let end_point = "https://api.dandanplay.net"; let API_comment = "/api/v2/comment/"; let API_search_episodes = `/api/v2/search/episodes`; function get_episodeId(animeId, id) { id = id.padStart(4, "0"); let episodeId = `${animeId}${id}`; return episodeId; } async function get_search_episodes(anime, episode2) { const res = await request({ url: `${end_point}${API_search_episodes}`, params: { anime, episode: episode2 } }); return res.animes; } async function get_comment(episodeId) { const res = await request({ url: `${end_point}${API_comment}${episodeId}?withRelated=true&chConvert=1` }); return res.comments; } const key = CryptoJS.enc.Utf8.parse("57A891D97E332A9D"); const iv = CryptoJS.enc.Utf8.parse("844182a9dfe9c5ca"); async function get_yhdmjx_url(url2) { let body = await xhr_get(url2); let m3u8 = get_m3u8_url(body); if (m3u8) { let body2 = await xhr_get(m3u8); let aes_data = get_encode_url(body2); if (aes_data) { let url3 = Decrypt(aes_data); let src = url3.split(".net/")[1]; let src_url2 = `http://v16m-default.akamaized.net/${src}`; console.log("原始地址:"); console.log(src_url2); return src_url2; } } } function get_m3u8_url(data) { let regex = /"url":"([^"]+)","url_next":"([^"]+)"/g; const matches = data.match(regex); if (matches) { let play = JSON.parse(`{${matches[0]}}`); let m3u8 = `https://danmu.yhdmjx.com/m3u8.php?url=${play.url}`; console.log(m3u8); return m3u8; } else { console.log("No matches found."); } } function get_encode_url(data) { let regex = /getVideoInfo\("([^"]+)"/; const matches = data.match(regex); if (matches) { return matches[1]; } else { console.log("No matches found."); } } function Decrypt(srcs) { let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8); return decryptedStr.toString(); } function update_danmu(art2, danmus) { art2.plugins.artplayerPluginDanmuku.config({ danmuku: danmus }); art2.plugins.artplayerPluginDanmuku.load(); } function add_danmu(art2) { let plug = artplayerPluginDanmuku({ danmuku: [], speed: 5, // 弹幕持续时间,单位秒,范围在[1 ~ 10] opacity: 1, // 弹幕透明度,范围在[0 ~ 1] fontSize: 25, // 字体大小,支持数字和百分比 color: "#FFFFFF", // 默认字体颜色 mode: 0, // 默认模式,0-滚动,1-静止 margin: [10, "25%"], // 弹幕上下边距,支持数字和百分比 antiOverlap: true, // 是否防重叠 useWorker: true, // 是否使用 web worker synchronousPlayback: false, // 是否同步到播放速度 filter: (danmu) => danmu.text.length < 50, // 弹幕过滤函数,返回 true 则可以发送 lockTime: 5, // 输入框锁定时间,单位秒,范围在[1 ~ 60] maxLength: 100, // 输入框最大可输入的字数,范围在[0 ~ 500] minWidth: 200, // 输入框最小宽度,范围在[0 ~ 500],填 0 则为无限制 maxWidth: 600, // 输入框最大宽度,范围在[0 ~ Infinity],填 0 则为 100% 宽度 theme: "light", // 输入框自定义挂载时的主题色,默认为 dark,可以选填亮色 light heatmap: true, // 是否开启弹幕热度图, 默认为 false beforeEmit: (danmu) => !!danmu.text.trim() // 发送弹幕前的自定义校验,返回 true 则可以发送 // 通过 mount 选项可以自定义输入框挂载的位置,默认挂载于播放器底部,仅在当宽度小于最小值时生效 // mount: document.querySelector('.artplayer-danmuku'), }); art2.plugins.add(plug); art2.on("artplayerPluginDanmuku:emit", (danmu) => { console.info("新增弹幕", danmu); }); art2.on("artplayerPluginDanmuku:error", (error) => { console.info("加载错误", error); }); art2.on("artplayerPluginDanmuku:config", (option) => { }); } function NewPlayer(src_url2) { re_render(); var art2 = new Artplayer({ container: ".artplayer-app", url: src_url2, // autoplay: true, // muted: true, autoSize: true, fullscreen: true, fullscreenWeb: true, autoOrientation: true, flip: true, playbackRate: true, aspectRatio: true, setting: true, controls: [ { position: "right", html: "上传弹幕", click: function() { const input = document.createElement("input"); input.type = "file"; input.accept = "text/xml"; input.addEventListener("change", () => { const reader = new FileReader(); reader.onload = () => { const xml = reader.result; let dm = bilibiliDanmuParseFromXml(xml); console.log(dm); art2.plugins.artplayerPluginDanmuku.config({ danmuku: dm }); art2.plugins.artplayerPluginDanmuku.load(); }; reader.readAsText(input.files[0]); }); input.click(); } } ], contextmenu: [ { name: "搜索", html: `<div id="k-player-danmaku-search-form"> <label> <span>搜索番剧名称</span> <input type="text" id="animeName" class="k-input" /> </label> <div style="min-height:24px; padding-top:4px"> <span id="tips"></span> </div> <label> <span>番剧名称</span> <select id="animes" class="k-select"></select> </label> <label> <span>章节</span> <select id="episodes" class="k-select"></select> </label> <label> <span class="open-danmaku-list"> <span>弹幕列表</span><small id="count"></small> </span> </label> <span class="specific-thanks">弹幕服务由 弹弹play 提供</span> </div>` } ] }); return art2; } function re_render() { let player = document.querySelector(".stui-player__video.clearfix"); if (player == void 0) { player = document.querySelector("#player-left"); } let div = player.querySelector("div"); let h = div.offsetHeight; let w = div.offsetWidth; player.removeChild(div); let app = `<div style="height: ${h}px; width: ${w}px;" class="artplayer-app"></div>`; player.innerHTML = app; } function getMode(key2) { switch (key2) { case 1: case 2: case 3: return 0; case 4: case 5: return 1; default: return 0; } } function bilibiliDanmuParseFromXml(xmlString) { if (typeof xmlString !== "string") return []; const matches = xmlString.matchAll(/<d (?:.*? )??p="(?<p>.+?)"(?: .*?)?>(?<text>.+?)<\/d>/gs); return Array.from(matches).map((match) => { const attr = match.groups.p.split(","); if (attr.length >= 8) { const text = match.groups.text.trim().replaceAll(""", '"').replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&"); return { text, time: Number(attr[0]), mode: getMode(Number(attr[1])), fontSize: Number(attr[2]), color: `#${Number(attr[3]).toString(16)}`, timestamp: Number(attr[4]), pool: Number(attr[5]), userID: attr[6], rowID: Number(attr[7]) }; } else { return null; } }).filter(Boolean); } function bilibiliDanmuParseFromJson(jsonString) { return jsonString.map((comment) => { let attr = comment.p.split(","); return { text: comment.m, time: Number(attr[0]), mode: getMode(Number(attr[1])), fontSize: Number(25), color: `#${Number(attr[2]).toString(16)}`, timestamp: Number(comment.cid), pool: Number(0), userID: attr[3], rowID: Number(0) }; }); } function createStorage(storage) { function getItem(key2, defaultValue) { try { const value = storage.getItem(key2); if (value) return JSON.parse(value); return defaultValue; } catch (error) { return defaultValue; } } return { getItem, setItem(key2, value) { storage.setItem(key2, JSON.stringify(value)); }, removeItem: storage.removeItem.bind(storage), clear: storage.clear.bind(storage) }; } createStorage(window.sessionStorage); const local = createStorage(window.localStorage); let gm; try { gm = { getItem: _GM_getValue, setItem: _GM_setValue }; } catch (error) { gm = local; } let url = window.location.href; let { episode, title } = get_anime_info(url); let animeUrl = url.split("-")[0]; console.log(url); console.log(episode); console.log(title); let info = local.getItem(animeUrl); if (info === void 0) { info = { // "animeTitle": title, "episodes": {}, "animes": [{ "animeTitle": title }], "idx": 0 }; } let src_url; if (!info["episodes"].hasOwnProperty(url)) { src_url = await( get_yhdmjx_url(url)); info["episodes"][url] = src_url; local.setItem(animeUrl, info); } else { src_url = info["episodes"][url]; } let art = NewPlayer(src_url); add_danmu(art); let $count = document.querySelector("#count"); let $animeName = document.querySelector("#animeName"); let $animes = document.querySelector("#animes"); let $episodes = document.querySelector("#episodes"); function art_msgs(msgs) { art.notice.show = msgs.join(",\n\n"); } let UNSEARCHED = [ "未搜索到番剧弹幕", "请按右键菜单", "手动搜索番剧名称" ]; let SEARCHED = () => [ `番剧:${$animes.options[$animes.selectedIndex].text}`, `章节: ${$episodes.options[$episodes.selectedIndex].text}`, `已加载 ${$count.textContent} 条弹幕` ]; init(); get_animes(); async function update_episode_danmu() { const episodeId = $episodes.value; console.log("episodeId: ", episodeId); let danmu = await get_comment(episodeId); let danmus = bilibiliDanmuParseFromJson(danmu); update_danmu(art, danmus); } function get_animes() { const { animes, idx } = info; const { animeTitle } = animes[idx]; if (!animes[idx].hasOwnProperty("animeId")) { console.log("没有缓存,请求接口"); get_animes_new(animeTitle); } else { console.log("有缓存,请求弹幕"); updateAnimes(animes, idx); } } async function get_animes_new(title2) { try { const animes = await get_search_episodes(title2); if (animes.length === 0) { art_msgs(UNSEARCHED); } else { info["animes"] = animes; local.setItem(animeUrl, info); } return animes; } catch (error) { console.log("弹幕服务异常,稍后再试"); } } function init() { art.on("artplayerPluginDanmuku:loaded", (danmus) => { console.info("加载弹幕", danmus.length); $count.textContent = danmus.length; if ($count.textContent === "") { art_msgs(UNSEARCHED); } else { art_msgs(SEARCHED()); } }); art.on("pause", () => { if ($count.textContent === "") { art_msgs(UNSEARCHED); } else { art_msgs(SEARCHED()); } }); $animeName.addEventListener("keypress", (e) => { if (e.key === "Enter") { get_animes_new($animeName.value); } }); $animeName.addEventListener("blur", () => { get_animes_new($animeName.value); }); $animeName.value = info["animes"][info["idx"]]["animeTitle"]; $animes.addEventListener("change", async () => { const idx_n = $animes.selectedIndex; const { idx, animes } = info; if (idx_n !== idx) { info["idx"] = idx_n; local.setItem(animeUrl, info); updateEpisodes(animes[idx_n]); } }); $episodes.addEventListener("change", update_episode_danmu); document.addEventListener("itemInserted", function(e) { let { animes: animes_o } = local.getItem(animeUrl); let { animes: animes_n, idx: idx_n } = JSON.parse(e.value); if (animes_n !== animes_o) { updateAnimes(animes_n, idx_n); } }); document.addEventListener("updateAnimes", function(e) { console.log("updateAnimes 事件"); updateEpisodes(e.value); }); document.addEventListener("updateEpisodes", function(e) { console.log("updateEpisodes 事件"); update_episode_danmu(); }); } function updateAnimes(animes, idx) { const html = animes.reduce( (html2, anime) => html2 + `<option value="${anime.animeId}">${anime.animeTitle}</option>`, "" ); $animes.innerHTML = html; $animes.value = animes[idx]["animeId"]; const event = new Event("updateAnimes"); event.value = animes[idx]; console.log(animes[idx]); document.dispatchEvent(event); } function updateEpisodes(anime) { const { animeId, episodes } = anime; const html = episodes.reduce( (html2, episode2) => html2 + `<option value="${episode2.episodeId}">${episode2.episodeTitle}</option>`, "" ); $episodes.innerHTML = html; let episodeId = get_episodeId(animeId, episode); $episodes.value = episodeId; const event = new Event("updateEpisodes"); document.dispatchEvent(event); } })(CryptoJS, artplayerPluginDanmuku, Artplayer);