动漫弹幕播放

自动匹配加载动漫剧集对应弹幕并播放,目前支持樱花动漫、风车动漫

当前为 2024-07-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         动漫弹幕播放
// @namespace    https://github.com/LesslsMore/anime-danmu-play
// @version      0.3.6
// @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
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/dexie.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
// ==/UserScript==

(async function (CryptoJS, artplayerPluginDanmuku, Artplayer, 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(url2) {
    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("没有自动匹配到动漫名称");
    }
    return {
      episode: episode2,
      title: title2
    };
  }
  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 = `<div style="height: ${h}px; width: ${w}px;" class="${container}"></div>`;
    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://api.dandanplay.net";
  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 = 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, { 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, 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 = "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 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("&quot;", '"').replaceAll("&apos;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
        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;
  }
  const db_name = "anime";
  const db_schema = {
    info: "&anime_id",
    // 主键 索引
    url: "&anime_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;
  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) {
    const item = {
      anime_id: key2,
      value
    };
    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;
    }
    return item.value;
  };
  let url = window.location.href;
  let { episode, title } = get_anime_info(url);
  let anime_url = url.split("-")[0];
  let anime_id = parseInt(anime_url.split("/")[4]);
  console.log(url);
  console.log(episode);
  console.log(title);
  let db_anime_url = {
    "episodes": {}
  };
  let db_url_value = await( db_url.get(anime_id));
  if (db_url_value != null) {
    db_anime_url = db_url_value;
  }
  let src_url;
  if (!db_anime_url["episodes"].hasOwnProperty(url)) {
    src_url = await( get_yhdmjx_url(url));
    if (src_url) {
      db_anime_url["episodes"][url] = src_url;
      db_url.put(anime_id, db_anime_url);
    }
  } else {
    src_url = db_anime_url["episodes"][url];
  }
  let db_anime_info = {
    "animes": [{ "animeTitle": title }],
    "idx": 0,
    "episode_dif": 0
  };
  let db_info_value = await( db_info.get(anime_id));
  if (db_info_value != null) {
    db_anime_info = db_info_value;
  } else {
    db_info.put(anime_id, db_anime_info);
  }
  console.log("db_anime_info", db_anime_info);
  console.log("src_url", src_url);
  re_render("artplayer-app");
  let art = NewPlayer(src_url, ".artplayer-app");
  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 = () => {
    try {
      return [`番剧:${$animes.options[$animes.selectedIndex].text}`, `章节: ${$episodes.options[$episodes.selectedIndex].text}`, `已加载 ${$count.textContent} 条弹幕`];
    } catch (e) {
      console.log(e);
      return [];
    }
  };
  init();
  get_animes();
  async function update_episode_danmu() {
    const new_idx = $episodes.selectedIndex;
    const db_anime_info2 = await db_info.get(anime_id);
    const { episode_dif } = db_anime_info2;
    let dif = new_idx + 1 - episode;
    if (dif !== episode_dif) {
      db_anime_info2["episode_dif"] = dif;
      db_info.put(anime_id, db_anime_info2);
    }
    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 } = db_anime_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 {
        db_anime_info["animes"] = animes;
        db_info.put(anime_id, db_anime_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 = db_anime_info["animes"][db_anime_info["idx"]]["animeTitle"];
    $animes.addEventListener("change", async () => {
      const new_idx = $animes.selectedIndex;
      const { idx, animes } = db_anime_info;
      if (new_idx !== idx) {
        db_anime_info["idx"] = new_idx;
        db_info.put(anime_id, db_anime_info);
        updateEpisodes(animes[new_idx]);
      }
    });
    $episodes.addEventListener("change", update_episode_danmu);
    document.addEventListener("db_info_put", async function(e) {
      let { animes: old_animes } = await db_info.get(anime_id);
      let { animes: new_animes, idx: new_idx } = e.value;
      if (new_animes !== old_animes) {
        updateAnimes(new_animes, new_idx);
      }
    });
    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);
  }
  async function updateEpisodes(anime) {
    const { animeId, episodes } = anime;
    const html = episodes.reduce((html2, episode2) => html2 + `<option value="${episode2.episodeId}">${episode2.episodeTitle}</option>`, "");
    $episodes.innerHTML = html;
    const db_anime_info2 = await db_info.get(anime_id);
    const { episode_dif } = db_anime_info2;
    let episodeId = get_episodeId(animeId, episode_dif + episode);
    $episodes.value = episodeId;
    const event = new Event("updateEpisodes");
    document.dispatchEvent(event);
  }

})(CryptoJS, artplayerPluginDanmuku, Artplayer, Dexie);