Bangumi Mio Tools

Bangumi tools with mio

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name       Bangumi Mio Tools
// @namespace  remio/script-bangumi
// @version    1.0.0
// @author     kasuie
// @license    MIT
// @icon       https://www.google.com/s2/favicons?sz=64&domain=bgm.tv
// @match      https://bgm.tv/*
// @grant      GM.deleteValue
// @grant      GM.getValue
// @grant      GM.listValues
// @grant      GM.setValue
// @grant      GM.xmlHttpRequest
// @description Bangumi tools with mio
// ==/UserScript==

(function () {
  'use strict';

  var _GM = /* @__PURE__ */ (() => typeof GM != "undefined" ? GM : void 0)();
  const storage = {
    set(key, val) {
      return _GM.setValue(key, val);
    },
    get(key, defaultValue) {
      return _GM.getValue(key, defaultValue);
    },
    all() {
      return _GM.listValues();
    },
    del(key) {
      return _GM.deleteValue(key);
    },
    async clear() {
      let keys = this.all();
      for (let key of await keys) {
        _GM.deleteValue(key);
      }
    }
  };
  const request = (data) => {
    return new Promise((resolve, reject) => {
      if (!data.method) {
        data.method = "get";
      }
      if (!data.timeout) {
        data.timeout = 6e4;
      }
      data.onload = function(res) {
        try {
          resolve(JSON.parse(res.responseText));
        } catch (error) {
          reject(error);
        }
      };
      data.onerror = function(e) {
        reject(e);
      };
      data.ontimeout = function() {
        reject("timeout");
      };
      _GM.xmlHttpRequest(data);
    });
  };
  let $message, $button, global;
  const BaseUrl = "https://example.com";
  const onMessage = (text, type = "success", time = 3e3) => {
    if (text && $message) {
      let timer;
      if (timer) clearTimeout(timer);
      if (type == "success") {
        $message.css("color", "#4ef16a");
      } else if (type == "error") {
        $message.css("color", "#f23939");
      } else {
        $message.css("color", "#ffffff");
      }
      $message.text(text);
      $message.css("top", "36px");
      timer = setTimeout(() => {
        $message.css("top", "-36px");
        $message.text("");
      }, time);
    }
  };
  const onLoading = (loading = true) => {
    const $submit = $("#apisubmit");
    $button && $button.attr({
      disabled: loading
    });
    $submit && $submit.attr({
      disabled: loading
    });
  };
  const onGetPathEnd = (type, _path = null) => {
    const path = _path || window.location.pathname;
    if (path && path.includes(type)) {
      const pathname = path.split("/");
      if (pathname == null ? void 0 : pathname.length) {
        return _path ? pathname[pathname.length - 1] : +pathname[pathname.length - 1];
      } else {
        return null;
      }
    } else {
      return null;
    }
  };
  const init = () => {
    $button = $("<button>", {
      id: "mio-button",
      text: "Mio"
    });
    $message = $("<div>", {
      id: "mio-message"
    });
    const styles = `
      .modal {
          display: none; /* 默认隐藏 */
          position: fixed;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          width: 420px;
          background-color: #010101;
          box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
          padding: 20px;
          border-radius: 10px;
          z-index: 1000;
      }
      .modal-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          font-size: 18px;
          font-weight: bold;
      }
      .submit-btn {
        padding: 6px 16px;
        font-size: 12px;
        background-color: #f09199;
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
      }
      .close-btn {
          cursor: pointer;
          font-size: 24px;
      }
      .modal-body {
          margin: 20px 0;
          min-height: 200px;
      }
      .modal-footer {    
          display: flex;
          align-items: center;
          justify-content: flex-end;
          gap: 16px;
      }
      .overlay {
          display: none; /* 默认隐藏 */
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background: rgba(0, 0, 0, 0.5);
          z-index: 999;
      }
      .open-modal-btn {
          padding: 10px 20px;
          font-size: 16px;
          cursor: pointer;
      }
      #rank {
        outline: none;
        border: none;
        background-color: rgba(255, 255, 255, .3);
        width: 80px;
        border-radius: 8px;
        padding: 6px 8px;
      }
      #api {
        outline: none;
        border: none;
        background-color: rgba(255, 255, 255, .3);
        width: 180px;
        border-radius: 8px;
        padding: 6px 8px;
      }
      `;
    $("head").append(`<style>${styles}</style>`);
    $("body.bangumi").append(`
                <div class="overlay"></div>
                <div class="modal">
                    <div class="modal-header">
                        <span class="modal-title">添加</span>
                        <span class="close-btn">&times;</span>
                    </div>
                    <div class="modal-body">
                    </div>
                    <div class="modal-footer">
                        <input id="api" type="text" placeholder="提交接口" />
                        <input id="rank" type="number" placeholder="排名" />
                        <button id="apisubmit" class="submit-btn">提交</button>
                    </div>
                </div>
            `);
    $(".close-btn").on("click", onCloseModal);
    $(".submit-btn").on("click", function() {
      const rank = +($("#rank").val() || -1);
      const { params, url } = global;
      if (rank) {
        onAppend("div.modal-body", "<p>获取到排名信息...</p>");
      }
      onSubmit(url, {
        ...params,
        rank: rank ? rank : null
      });
    });
    $button.on("click", function() {
      onGetData();
      $(".overlay, .modal").fadeIn();
    });
    $button.css({
      padding: "6px 16px",
      "font-size": "12px",
      "background-color": "#f09199",
      color: "white",
      border: "none",
      "border-radius": "8px",
      cursor: "pointer",
      position: "fixed",
      top: "14px",
      right: "20px"
    });
    $message.css({
      position: "fixed",
      top: "-36px",
      left: "50%",
      transform: "translateX(-50%)",
      width: "200px",
      textAlign: "center",
      minWidth: "100px",
      minHeight: "36px",
      background: "rgba(0,0,0,.4)",
      borderRadius: "12px",
      transition: "all .3s ease-in-out",
      display: "flex",
      justifyContent: "center",
      alignItems: "center"
    });
    $("body.bangumi").append($button);
    $("body.bangumi").append($message);
  };
  const CID = onGetPathEnd("character");
  const SID = onGetPathEnd("subject");
  console.log("CID>>>", CID, "SID>>>", SID);
  const onAppend = (ele, data) => $(ele).append(data);
  const onClear = (ele) => $(ele).html("");
  const onCloseModal = () => {
    $("#rank").val("");
    onLoading(false);
    onClear("div.modal-body");
    $(".overlay, .modal").fadeOut();
  };
  const onGetData = async () => {
    const CharApi = await storage.get("CharApi", `${BaseUrl}/bgm/saveChar`);
    const SubApi = await storage.get("SubApi", `${BaseUrl}/bgm/saveSub`);
    console.log(CharApi, SubApi, "CharApi, SubApi");
    if (CID) {
      $("span.modal-title").text("新增角色");
      onLoading();
      onAppend("div.modal-body", "<p>开始加载数据...</p>");
      const character = request({
        method: "GET",
        url: `https://api.bgm.tv/v0/characters/${CID}`
      });
      request({
        method: "GET",
        url: `https://api.bgm.tv/v0/characters/${CID}/persons`
      }).then((cvs) => {
        let cv = null;
        const item = cvs == null ? void 0 : cvs.find((v) => v.subject_type === 2);
        if (item == null ? void 0 : item.id) {
          cv = request({
            method: "GET",
            url: `https://api.bgm.tv/v0/persons/${item.id}`
          });
        }
        Promise.all([character, cv]).then(([character2, cv2]) => {
          const cvData = cv2 && formatPerson(cv2, true);
          const params = {
            ...formatChar(character2),
            actors: cvData ? JSON.stringify({ name: cvData.name, id: cvData.id }) : null,
            cv: cvData
          };
          global = {
            params,
            url: CharApi
          };
          onLoading(false);
          onAppend("div.modal-body", "<p>加载成功</p>");
          $("#api").val((global == null ? void 0 : global.url) || "");
        });
      });
    } else if (SID) {
      $("span.modal-title").text("新增番剧");
      onLoading();
      onAppend("div.modal-body", "<p>开始加载数据...</p>");
      const subject = request({
        method: "GET",
        url: `https://api.bgm.tv/v0/subjects/${SID}`
      });
      const characters = request({
        method: "GET",
        url: `https://api.bgm.tv/v0/subjects/${SID}/characters`
      });
      const persons = request({
        method: "GET",
        url: `https://api.bgm.tv/v0/subjects/${SID}/persons`
      });
      Promise.all([subject, characters, persons]).then(
        ([subject2, characters2, persons2]) => {
          const afterPersons = formatRoles(persons2);
          const { data, actors } = formatRoles(characters2, true);
          const personsId = [], resultPersons = [];
          const relCharacters = data.map((v) => ({
            id: v.id,
            name: v.name,
            relation: v.relation
          }));
          const relPersons = afterPersons.map((v) => {
            if (!personsId.includes(v.id)) {
              personsId.push(v.id);
              resultPersons.push({
                ...v,
                summary: null
              });
            }
            return {
              id: v.id,
              name: v.name,
              position: v.summary
            };
          });
          const resultActors = (actors == null ? void 0 : actors.reduce(
            (prev, curr) => {
              if (!prev[0].includes(curr.id)) {
                prev[0].push(curr.id);
                prev[1].push(curr);
              }
              return prev;
            },
            [[], []]
          )[1]) || [];
          const params = {
            ...formatSub(subject2),
            characters: data,
            persons: resultPersons.concat(resultActors),
            relPersons: JSON.stringify(relPersons),
            relCharacters: JSON.stringify(relCharacters)
          };
          global = {
            params,
            url: SubApi
          };
          onLoading(false);
          onAppend("div.modal-body", "<p>加载成功</p>");
          $("#api").val((global == null ? void 0 : global.url) || "");
        }
      );
    }
  };
  const onSubmit = (url, params, options = {
    method: "POST"
  }) => {
    onAppend("div.modal-body", "<p>开始上报数据...</p>");
    const Api = $("#api").val();
    if (Api !== url) {
      if (CID) {
        storage.set("CharApi", Api);
      } else if (SID) {
        storage.set("SubApi", Api);
      }
    }
    console.log("onSubmit:", params);
    request({
      ...options,
      url,
      headers: { "Content-Type": "application/json" },
      data: JSON.stringify(params)
    }).then((res) => {
      console.log(res, "请求结果~");
      onAppend(
        "div.modal-body",
        `<p>上报数据结果:${res.success ? "成功" : "失败"}</p>`
      );
      onMessage(res.message, res.success ? "success" : "error");
    }).catch((e) => onMessage(`请求失败:${e}`, "error")).finally(
      () => setTimeout(() => {
        onCloseModal();
      }, 300)
    );
  };
  const formatPerson = (person, isCv) => {
    let {
      career,
      blood_type,
      birth_day,
      birth_mon,
      birth_year,
      images,
      infobox,
      gender,
      stat,
      last_modified,
      img,
      ...others
    } = person;
    for (const key in images) {
      images[key] = images[key].replace("https://lain.bgm.tv", "");
    }
    const infoKeys = [
      { name: "中文名", field: "nameCn" },
      { name: "别名", field: "aliasName" },
      { name: "性别", field: "gender" },
      { name: "生日", field: "birth" },
      { name: "Twitter", field: "twitter" },
      { name: "引用来源", field: "source" }
    ];
    let aliasName, nameCn, twiAccount, birthDay;
    const info = infobox == null ? void 0 : infobox.reduce((prev, curr) => {
      const item = infoKeys.find((v) => curr.key.includes(v.name));
      if (item) {
        if (item.field === "nameCn" && !nameCn) {
          nameCn = curr.value;
        } else if (item.field === "aliasName" && !aliasName) {
          aliasName = curr.value;
        } else if (item.field === "gender" && !gender) {
          gender = curr.value === "男" ? "male" : "female";
        } else if (item.field === "twitter") {
          if (curr.value.includes(".com")) {
            twiAccount = onGetPathEnd(".com", curr.value);
          } else {
            twiAccount = curr.value;
          }
        } else if (item.field === "birth") {
          birthDay = curr.value;
        }
      } else {
        !prev && (prev = {});
        prev[curr.key] = curr.value;
      }
      return prev;
    }, null);
    if (!birthDay) {
      birthDay = [birth_year, birth_mon, birth_day].reduce((prev, curr) => {
        return prev ? `${prev}-${curr || ""}` : curr;
      }, null);
    }
    return {
      ...others,
      twiAccount,
      birthDay,
      nameCn,
      gender,
      isCv,
      info: JSON.stringify(info),
      aliasName: aliasName && JSON.stringify(aliasName),
      bgmImages: JSON.stringify(images),
      career: career && career.join(",")
    };
  };
  const formatChar = (char) => {
    let {
      birth_day,
      birth_mon,
      birth_year,
      blood_type,
      images,
      infobox,
      stat,
      gender,
      ...others
    } = char;
    for (const key in images) {
      images[key] = images[key].replace("https://lain.bgm.tv", "");
      if (key === "large") {
        images["avatar"] = images[key].replace("/pic/crt/l", "/pic/crt/g");
      }
    }
    const infoKeys = [
      { name: "中文名", field: "nameCn" },
      { name: "别名", field: "aliasName" },
      { name: "性别", field: "gender" },
      { name: "生日", field: "birth" },
      { name: "引用来源", field: "source" }
    ];
    let aliasName, nameCn, birthDay, birthDesc;
    birthDay = [birth_year, birth_mon, birth_day].reduce((prev, curr) => {
      return prev ? `${prev}-${curr || ""}` : curr;
    }, null);
    const info = infobox == null ? void 0 : infobox.reduce((prev, curr) => {
      const item = infoKeys.find((v) => curr.key.includes(v.name));
      if (item) {
        if (item.field === "nameCn" && !nameCn) {
          nameCn = curr.value;
        } else if (item.field === "aliasName" && !aliasName) {
          aliasName = curr.value;
        } else if (item.field === "gender" && !gender) {
          gender = curr.value === "男" ? "male" : "female";
        } else if (item.field === "birth") {
          birthDesc = curr.value;
        }
      } else {
        !prev && (prev = {});
        prev[curr.key] = curr.value;
      }
      return prev;
    }, null);
    return {
      ...others,
      info: info && JSON.stringify(info),
      nameCn,
      gender,
      birthDay,
      birthDesc,
      bloodType: blood_type,
      aliasName: aliasName && JSON.stringify(aliasName),
      bgmImages: JSON.stringify(images)
    };
  };
  const formatSub = (subject) => {
    var _a, _b;
    let {
      id,
      tags,
      images,
      name_cn,
      rating: { score, total },
      date,
      infobox,
      total_episodes,
      collection,
      ...others
    } = subject;
    let aliasName, openTv, otherTv, startDate, endDate, copyright, official, original, director, charSetting;
    for (const key in images) {
      images[key] = (_a = images[key]) == null ? void 0 : _a.replace("https://lain.bgm.tv", "");
      if (key === "large") {
        images["avatar"] = (_b = images[key]) == null ? void 0 : _b.replace("/pic/cover/l", "/pic/cover/g");
      }
    }
    const tagsString = tags == null ? void 0 : tags.reduce((prev, curr, index) => {
      return index > 15 ? prev : prev ? `${prev},${curr.name}` : curr.name;
    }, "");
    const infoBox = infobox == null ? void 0 : infobox.filter((item) => {
      if (item.key === "中文名") {
        !name_cn && (name_cn = item.value);
        return false;
      } else if (item.key === "别名") {
        aliasName = JSON.stringify(item.value);
        return false;
      } else if (item.key === "话数") {
        !total_episodes && (total_episodes = +item.value);
        return false;
      } else if (item.key === "放送开始") {
        startDate = onGetDate(item.value) || item.value;
        return false;
      } else if (item.key === "播放结束") {
        endDate = onGetDate(item.value) || item.value;
        return false;
      } else if (item.key === "官方网站") {
        official = item.value;
        return false;
      } else if (item.key === "播放电视台") {
        openTv = item.value;
        return false;
      } else if (item.key === "其他电视台") {
        otherTv = item.value;
        return false;
      } else if (item.key === "Copyright") {
        copyright = item.value;
        return false;
      } else if (item.key === "原作") {
        original = item.value;
        return false;
      } else if (item.key === "导演") {
        director = item.value;
        return false;
      } else if (item.key === "人物设定" || item.key === "角色设定") {
        charSetting = item.value;
        return false;
      } else {
        return true;
      }
    });
    return {
      ...others,
      bgmId: id,
      nameCn: name_cn,
      tags: tagsString,
      bgmScore: score,
      bgmRaters: total,
      infoBox: JSON.stringify(infoBox),
      startDate,
      endDate,
      aliasName,
      broadcast: JSON.stringify({
        openTv,
        otherTv
      }),
      info: JSON.stringify({
        copyright,
        official,
        original,
        director,
        charSetting
      }),
      bgmImages: JSON.stringify(images),
      totalEpisodes: total_episodes
    };
  };
  const formatRoles = (list, isChar = false, isCv = false) => {
    if (!Array.isArray(list)) return null;
    let data = list;
    if (isChar) {
      let totalActors = [];
      data = list.map((v) => {
        let { actors, images, locked, ...others } = v;
        for (const key in images) {
          images[key] = images[key].replace("https://lain.bgm.tv", "");
          if (key === "large") {
            images["avatar"] = images[key].replace("/pic/crt/l", "/pic/crt/g");
          }
        }
        const itemCvs = formatRoles(actors, false, true);
        totalActors = totalActors.concat(itemCvs);
        return {
          ...others,
          locked: locked || false,
          isCv,
          actors: (itemCvs == null ? void 0 : itemCvs.length) ? JSON.stringify(
            itemCvs.map((v2) => ({ name: v2.name, id: v2.id }))
          ) : null,
          bgmImages: JSON.stringify(images)
        };
      });
      return { data, actors: totalActors };
    } else {
      const whiteListByCount = ["作画监督", "副导演", "原画", "补间动画"];
      const whiteList = [
        "原作",
        "导演",
        "人物原案",
        "人物设定",
        "音乐",
        "总作画监督",
        "脚本",
        "系列构成",
        "美术监督",
        "摄影监督"
      ];
      const relMaps = list.reduce((prev, curr) => {
        if (Object.hasOwn(prev, curr.relation)) {
          ++prev[curr.relation];
        } else {
          prev[curr.relation] = 1;
        }
        return prev;
      }, {});
      if (!isCv) {
        data = list.filter((v) => {
          if (!v.relation) return false;
          if (whiteList.includes(v.relation)) {
            return true;
          } else if (whiteListByCount.includes(v.relation)) {
            return relMaps[v.relation] < 7;
          } else {
            return false;
          }
        });
      }
      data = data.map((v) => {
        let { career, images, short_summary, locked, relation, ...others } = v;
        for (const key in images) {
          images[key] = images[key].replace("https://lain.bgm.tv", "");
          if (key === "large") {
            images["avatar"] = images[key].replace("/pic/crt/l", "/pic/crt/g");
          }
        }
        return {
          ...others,
          locked: locked || false,
          isCv,
          summary: isCv ? short_summary : relation,
          bgmImages: JSON.stringify(images),
          career: career && career.join(",")
        };
      });
      return data;
    }
  };
  const onGetDate = (dateString) => {
    if (!dateString) return null;
    const dateParts = dateString.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
    if (dateParts) {
      const year = parseInt(dateParts[1], 10);
      const month = parseInt(dateParts[2], 10) - 1;
      const day = parseInt(dateParts[3], 10);
      return new Date(year, month, day);
    } else {
      return null;
    }
  };
  init();

})();