// ==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">×</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();
})();