@animespace/bangumi
Version:
Create your own Anime Space
264 lines (259 loc) • 8.95 kB
JavaScript
import prompts from 'prompts';
import path from 'path';
import fs from 'fs-extra';
import { uniqBy, ufetch } from '@animespace/core';
import { fetchResources } from '@animegarden/client';
import { lightBlue, bold, lightRed } from '@breadc/color';
import { format, getYear, subMonths } from 'date-fns';
import { BgmClient } from 'bgmc';
const version = "0.1.0-beta.24";
const client = new BgmClient(ufetch, {
maxRetry: 1,
userAgent: `animespace/${version} (https://github.com/yjl9903/AnimeSpace)`
});
async function generatePlan(system, collections, options) {
const output = [];
const writeln = (text) => {
if (options.create) {
output.push(text);
} else {
console.log(text);
}
};
const now = /* @__PURE__ */ new Date();
const date = inferDate(options.date);
writeln(`title: \u521B\u5EFA\u4E8E ${format(now, "yyyy-MM-dd hh:mm")}`);
writeln(``);
writeln(`date: ${format(date, "yyyy-MM-dd hh:mm")}`);
writeln(``);
writeln(`status: onair`);
writeln(``);
writeln(`onair:`);
for (const anime of collections) {
if (typeof anime === "object") {
const begin = anime.subject?.date ? new Date(anime.subject.date) : void 0;
if (begin && begin.getTime() < date.getTime()) {
continue;
}
if (options.create) {
system.logger.log(
`${lightBlue("Searching")} ${bold(
anime.subject?.name_cn || anime.subject?.name || `Bangumi ${anime.subject_id}`
)}`
);
}
}
try {
const item = await client.subject(typeof anime === "object" ? anime.subject_id : anime);
const title = item.name_cn || item.name;
const aliasBox = item.infobox?.find((box) => box.key === "\u522B\u540D");
const translations = Array.isArray(aliasBox?.value) ? aliasBox?.value.map((v) => v?.v).filter(Boolean) ?? [] : typeof aliasBox?.value === "string" ? [aliasBox.value] : [];
if (item.name && item.name !== title) {
translations.unshift(item.name);
}
const plan = {
title,
bgm: "" + item.id,
season: inferSeason(title, ...translations),
type: inferType(item),
translations
};
const escapeString = (t) => t.replace(`'`, `''`);
writeln(` - title: ${escapeString(plan.title)}`);
writeln(` alias:`);
for (const t of plan.translations ?? []) {
writeln(` - '${escapeString(t)}'`);
}
if (plan.season !== 1) {
writeln(` season: ${plan.season}`);
}
writeln(` bgm: '${plan.bgm}'`);
if (plan.type) {
writeln(` type: '${plan.type}'`);
}
if (options.fansub) {
const fansub = await getFansub([plan.title, ...plan.translations]);
writeln(` fansub:`);
if (fansub.length === 0) {
writeln(` # No fansub found, please check the translations or search keywords`);
}
for (const f of fansub) {
writeln(` - ${f}`);
}
if (fansub.length === 0 && options.create) {
system.logger.warn(`No fansub found for ${title}`);
}
}
const includeURL = [title, ...translations].map(
(t) => t.replace(/\[/g, "%5B").replace(/\]/g, "%5D").replace(/,/g, "%2C").replace(/"/g, "%22").replace(/ /g, "%20")
).map((v) => "include=" + v);
writeln(
` # https://animes.garden/resources/1?${includeURL.join("&")}&after=${encodeURIComponent(
date.toISOString()
)}`
);
writeln(``);
} catch (error) {
if (typeof anime === "object") {
system.logger.error(
`${lightRed("Failed to search")} ${bold(
anime.subject?.name_cn || anime.subject?.name || `Bangumi ${anime.subject_id}`
)}`
);
} else {
system.logger.error(error);
}
}
}
if (options.create) {
const p = path.join(system.space.root.resolve(options.create).path);
await fs.writeFile(p, output.join("\n"), "utf-8");
}
}
async function searchBgm(input) {
return (await client.search(input, { type: 2 })).list ?? [];
}
async function getCollections(username) {
const list = [];
while (true) {
const { data } = await client.getCollections(username, {
subject_type: 2,
type: 3,
limit: 50,
offset: list.length
});
if (data && data.length > 0) {
list.push(...data);
} else {
break;
}
}
return uniqBy(list, (c) => "" + c.subject_id);
}
async function getFansub(titles) {
const { resources } = await fetchResources({
fetch: ufetch,
include: titles,
count: -1,
retry: 5
});
return uniqBy(
resources.filter((r) => !!r.fansub),
(r) => r.fansub.name
).map((r) => r.fansub.name);
}
function inferType(subject) {
const FILM = ["\u7535\u5F71", "\u5267\u573A\u7248"];
const titles = [subject.name, subject.name_cn];
{
for (const title of titles) {
for (const f of FILM) {
if (title && title.includes(f)) {
return "\u7535\u5F71";
}
}
}
}
{
for (const tag of subject.tags) {
if (FILM.includes(tag.name)) {
return "\u7535\u5F71";
}
}
}
return void 0;
}
function inferSeason(...titles) {
for (const title of titles) {
{
const match = /Season\s*(\d+)/.exec(title);
if (match) {
return +match[1];
}
}
{
const match = /第\s*(\d+)\s*(季|期)/.exec(title);
if (match) {
return +match[1];
}
}
if (title.includes("\u7B2C\u4E8C\u5B63")) return 2;
if (title.includes("\u7B2C\u4E09\u5B63")) return 3;
if (title.includes("\u7B2C\u56DB\u5B63")) return 4;
if (title.includes("\u7B2C\u4E94\u5B63")) return 5;
if (title.includes("\u7B2C\u516D\u5B63")) return 6;
if (title.includes("\u7B2C\u4E03\u5B63")) return 7;
if (title.includes("\u7B2C\u516B\u5B63")) return 8;
if (title.includes("\u7B2C\u4E5D\u5B63")) return 9;
if (title.includes("\u7B2C\u5341\u5B63")) return 10;
}
return 1;
}
function inferDate(now) {
const date = !!now ? new Date(now) : /* @__PURE__ */ new Date();
const d1 = new Date(getYear(date), 1, 1, 0, 0, 0);
const d2 = new Date(getYear(date), 4, 1, 0, 0, 0);
const d3 = new Date(getYear(date), 7, 1, 0, 0, 0);
const d4 = new Date(getYear(date), 10, 1, 0, 0, 0);
const d5 = new Date(getYear(date) + 1, 1, 1, 0, 0, 0);
if (d1.getTime() > date.getTime()) {
return subMonths(d1, 1);
} else if (d2.getTime() > date.getTime()) {
return subMonths(d2, 1);
} else if (d3.getTime() > date.getTime()) {
return subMonths(d3, 1);
} else if (d4.getTime() > date.getTime()) {
return subMonths(d4, 1);
} else {
return subMonths(d5, 1);
}
}
function Bangumi(options) {
const defaultUsername = options.username;
return {
name: "bangumi",
options,
command(system, cli) {
const logger = system.logger.withTag("bangumi");
cli.command("bangumi search <input>", "Search anime from bangumi and generate plan").alias("bgm search").option("--date <date>", "Specify the onair begin date").option("--fansub", "Generate fansub list").action(async (input, options2) => {
const bgms = await searchBgm(input);
if (bgms.length === 0) {
logger.warn("\u672A\u627E\u5230\u4EFB\u4F55\u52A8\u753B");
return;
}
const selected = bgms.length === 1 ? { bangumi: bgms } : await prompts({
type: "multiselect",
name: "bangumi",
message: "\u9009\u62E9\u5C06\u8981\u751F\u6210\u8BA1\u5212\u7684\u52A8\u753B",
choices: bgms.map((bgm) => ({
title: (bgm.name_cn || bgm.name) ?? String(bgm.id),
value: bgm
})),
hint: "- \u4E0A\u4E0B\u79FB\u52A8, \u7A7A\u683C\u9009\u62E9, \u56DE\u8F66\u786E\u8BA4",
// @ts-ignore
instructions: false
});
if (!selected.bangumi) {
return;
}
if (bgms.length > 1) {
logger.log("");
}
await generatePlan(
system,
selected.bangumi.map((bgm) => bgm.id),
{ create: void 0, fansub: options2.fansub, date: options2.date }
);
});
cli.command("bangumi generate", "Generate Plan from your bangumi collections").alias("bgm gen").alias("bgm generate").option("--username <username>", "Bangumi username").option("--create <filename>", "Create plan file in the space directory").option("--fansub", "Generate fansub list").option("--date <date>", "Specify the onair begin date").action(async (options2) => {
const username = options2.username ?? defaultUsername ?? "";
if (!username) {
logger.error("You should provide your bangumi username with --username <username>");
}
const collections = await getCollections(username);
return await generatePlan(system, collections, options2);
});
}
};
}
export { Bangumi };