// ==UserScript== // @name 动漫弹幕播放 // @namespace https://github.com/LesslsMore/anime-danmu-play // @version 0.3.9 // @author lesslsmore // @description 自动匹配加载动漫剧集对应弹幕并播放,目前支持樱花动漫、风车动漫 // @license MIT // @icon https://cdn.yinghuazy.xyz/webjs/stui_tpl/statics/img/favicon.ico // @include /^https:\/\/www\.dmla.*\.com\/play\/.*$/ // @include https://www.tt776b.com/play/* // @include https://www.dm539.com/play/* // @require https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.js // @require https://cdn.jsdelivr.net/npm/artplayer@5.1.1/dist/artplayer.js // @require https://cdn.jsdelivr.net/npm/artplayer-plugin-danmuku@5.0.1/dist/artplayer-plugin-danmuku.js // @require https://cdn.jsdelivr.net/npm/dexie@4.0.8/dist/dexie.min.js // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.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 // @downloadURL none // ==/UserScript== (async function (CryptoJS, artplayerPluginDanmuku, Artplayer, saveAs, Dexie) { '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() { let url2 = window.location.href; let episode2 = parseInt(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("没有自动匹配到动漫名称"); } let anime_url = url2.split("-")[0]; let anime_id2 = parseInt(anime_url.split("/")[4]); console.log({ anime_id: anime_id2, episode: episode2, title: title2, url: url2 }); return { anime_id: anime_id2, episode: episode2, title: title2, url: url2 }; } function re_render(container) { 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 = `
`; player.innerHTML = app; } 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://lesslsmore-api.vercel.app/proxy"; let API_comment = "/api/v2/comment/"; let API_search_episodes = `/api/v2/search/episodes`; function get_episodeId(animeId, id) { id = id.toString().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$1 = 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}`; 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", 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$1, { 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) => { }); } const db_name = "anime"; const db_schema = { info: "&anime_id", // 主键 索引 url: "&anime_id", // 主键 索引 danmu: "[anime_id+episode_id]" // 组合键 索引 }; const db_obj = { [db_name]: get_db(db_name, db_schema) }; const db_url = db_obj[db_name].url; const db_info = db_obj[db_name].info; const db_danmu = db_obj[db_name].danmu; function get_db(db_name2, db_schema2, db_ver = 1) { let db = new Dexie(db_name2); db.version(db_ver).stores(db_schema2); return db; } const db_url_put = db_url.put.bind(db_url); const db_url_get = db_url.get.bind(db_url); db_url.put = async function(key2, value, expiryInMinutes = 60) { const now = /* @__PURE__ */ new Date(); const item = { anime_id: key2, value, expiry: now.getTime() + expiryInMinutes * 6e4 }; const result = await db_url_put(item); const event = new Event("db_yhdm_put"); event.key = key2; event.value = value; document.dispatchEvent(event); return result; }; db_url.get = async function(key2) { const item = await db_url_get(key2); const event = new Event("db_yhdm_get"); event.key = key2; event.value = item ? item.value : null; document.dispatchEvent(event); if (!item) { return null; } const now = /* @__PURE__ */ new Date(); if (now.getTime() > item.expiry) { await db_url.delete(key2); return null; } return item.value; }; const db_info_put = db_info.put.bind(db_info); const db_info_get = db_info.get.bind(db_info); db_info.put = async function(key2, value, expiryInMinutes = 60 * 24 * 7) { const now = /* @__PURE__ */ new Date(); const item = { anime_id: key2, value, expiry: now.getTime() + expiryInMinutes * 6e4 }; const result = await db_info_put(item); const event = new Event("db_info_put"); event.key = key2; event.value = value; document.dispatchEvent(event); return result; }; db_info.get = async function(key2) { const item = await db_info_get(key2); const event = new Event("db_info_get"); event.key = key2; event.value = item ? item.value : null; document.dispatchEvent(event); if (!item) { return null; } const now = /* @__PURE__ */ new Date(); if (now.getTime() > item.expiry) { await db_info.delete(key2); return null; } return item.value; }; const db_danmu_put = db_danmu.put.bind(db_danmu); const db_danmu_get = db_danmu.get.bind(db_danmu); db_danmu.put = async function(anime_id2, episode_id, value, expiryInMinutes = 60 * 24 * 7) { const now = /* @__PURE__ */ new Date(); const item = { anime_id: anime_id2, episode_id, value, expiry: now.getTime() + expiryInMinutes * 6e4 }; const result = await db_danmu_put(item); const event = new Event("db_danmu_put"); event.key = key; event.value = value; document.dispatchEvent(event); return result; }; db_danmu.get = async function(anime_id2, episode_id) { const key2 = { anime_id: anime_id2, episode_id }; const item = await db_danmu_get(key2); const event = new Event("db_danmu_get"); event.key = key2; event.value = item ? item.value : null; document.dispatchEvent(event); if (!item) { return null; } const now = /* @__PURE__ */ new Date(); if (now.getTime() > item.expiry) { await db_danmu.delete(key2); return null; } return item.value; }; function NewPlayer(src_url2, container) { var art2 = new Artplayer({ container, 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 = ".json, .xml"; input.addEventListener("change", () => { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const content = reader.result; if (file.name.endsWith(".json")) { let json = JSON.parse(content); let comments; if (json.length === 1) { comments = json[0].comments; } else { comments = json; } const dm = bilibiliDanmuParseFromJson(comments); console.log("Parsed JSON danmaku:", dm); art2.plugins.artplayerPluginDanmuku.config({ danmuku: dm }); art2.plugins.artplayerPluginDanmuku.load(); } else if (file.name.endsWith(".xml")) { const dm = bilibiliDanmuParseFromXml(content); console.log("Parsed XML danmaku:", dm); art2.plugins.artplayerPluginDanmuku.config({ danmuku: dm }); art2.plugins.artplayerPluginDanmuku.load(); } else { console.error("Unsupported file format. Please upload a .json or .xml file."); } }; reader.readAsText(file); }); input.click(); } }, { position: "right", html: "下载", click: async function() { let $episodes2 = document.querySelector("#episodes"); const episodeId = $episodes2.value; let { anime_id: anime_id2, episode: episode2, title: title2, url: url2 } = get_anime_info(); let danmu = await db_danmu.get(anime_id2, episodeId); const blob = new Blob([JSON.stringify(danmu)], { type: "text/plain;charset=utf-8" }); saveAs(blob, `${title2} - ${episode2}.json`); } } ], contextmenu: [ { name: "搜索", html: `