summaryrefslogtreecommitdiff
path: root/src/util
diff options
context:
space:
mode:
authorFushihara <1039534+fushihara@users.noreply.github.com>2024-09-23 15:19:25 +0900
committerFushihara <1039534+fushihara@users.noreply.github.com>2024-09-23 15:19:25 +0900
commitba0c8d73e585d7ff249dd90684254bc7fa025544 (patch)
tree3b35be04e7bf11eb046246696d45fb8854f01f14 /src/util
parent41eff0687f69539a514cfa3a28a68f89fc8c54fd (diff)
commit
Diffstat (limited to 'src/util')
-rw-r--r--src/util/animeLoader.ts93
-rw-r--r--src/util/articleLoader.ts88
-rw-r--r--src/util/pagenation.ts91
3 files changed, 272 insertions, 0 deletions
diff --git a/src/util/animeLoader.ts b/src/util/animeLoader.ts
new file mode 100644
index 0000000..120123b
--- /dev/null
+++ b/src/util/animeLoader.ts
@@ -0,0 +1,93 @@
+import { readFile } from "fs/promises";
+import { z } from "zod";
+const zodType = z.array(
+ z.strictObject({
+ animeId: z.number().int().min(0),
+ title: z.string(),
+ primaryCategory: z.string(),
+ startSeason: z.string(),
+ titleReviewScore: z.object({
+ story: z.number().min(0).nullable(),
+ sakuga: z.number().min(0).nullable(),
+ character: z.number().min(0).nullable(),
+ music: z.number().min(0).nullable(),
+ originality: z.number().min(0).nullable(),
+ storyboard: z.number().min(0).nullable(),
+ voice: z.number().min(0).nullable(),
+ song: z.number().min(0).nullable(),
+ manzokudo: z.number().min(0).nullable(),
+ }),
+ titleReviewList: z.array(
+ z.object({
+ reviewId: z.number().int(),
+ userIconUrl: z.string().url(),
+ userName: z.string(),
+ score: z.number(),
+ isSpoiler: z.boolean(),
+ timestampSec: z.number().int().min(0),
+ commentCount: z.number().int().min(0),
+ iineCount: z.number().int().min(0)
+ }),
+ ),
+ titleHitokotoList: z.array(
+ z.object({
+ hitokotoId: z.number().int(),
+ userIcon: z.string().url(),
+ userName: z.string(),
+ reviewHtml: z.string(),
+ timestampSec: z.number().int(),
+ })
+ ),
+ episodeList: z.array(z.object({
+ episodeId: z.number().int().min(0),
+ subTitle: z.string(),
+ reviewCount: z.number().int().min(0),
+ score: z.number().nullable(),
+ })),
+ episodeHitokotoList: z.array(z.object({
+ episodeId: z.number().int().min(0),
+ hitokotoList: z.array(z.object({
+ hitokotoId: z.number().int().min(0),
+ userIcon: z.string(),
+ userName: z.string().min(1),
+ reviewHtml: z.string().min(1),
+ timestampSec: z.number().int().min(0),
+ }))
+ })),
+ })
+);
+const MAX_ITEM_LIMIT = process.env["AKIBA_SOUKEN_MAX_ITEM_LIMIT"];
+export type AnimeLoaderData = z.infer<typeof zodType>[number]
+class AnimeLoader {
+ constructor() { }
+ private dataCache: Awaited<ReturnType<AnimeLoader["loadData_"]>> | null = null;
+ async loadData() {
+ if (this.dataCache != null) {
+ return this.dataCache;
+ }
+ const loadedData = await this.loadData_();
+ this.dataCache = loadedData;
+ return loadedData;
+ }
+ private async loadData_() {
+ const articleJsonPath = process.env["AKIBA_SOUKEN_ANIME_JSON"]!;
+ //console.log(`[${articleJsonPath}]`);
+ const jsonStr = await readFile(articleJsonPath, { encoding: "utf-8" }).then(text => {
+ const jsonObj = JSON.parse(text);
+ return jsonObj;
+ });
+ const parsedObj = zodType.parse(jsonStr).filter(a => {
+ if (a.titleHitokotoList.length != 0) { return true; }
+ if (a.titleReviewList.length != 0) { return true; }
+ if (a.titleReviewScore.manzokudo != null) { return true; }
+ if (a.episodeList.find(e => e.score != null || e.reviewCount != 0)) { return true; }
+ if (a.episodeHitokotoList.length != 0) { return true; }
+ return false;
+ });
+ if (MAX_ITEM_LIMIT != null) {
+ parsedObj.length = Math.min(Number(MAX_ITEM_LIMIT), parsedObj.length);
+ }
+ return parsedObj;
+ }
+}
+export const animeLoader = new AnimeLoader(); \ No newline at end of file
diff --git a/src/util/articleLoader.ts b/src/util/articleLoader.ts
new file mode 100644
index 0000000..787725a
--- /dev/null
+++ b/src/util/articleLoader.ts
@@ -0,0 +1,88 @@
+import { readFile } from "fs/promises";
+import { z } from "zod";
+const zodType = z.array(
+ z.object({
+ articleId: z.number().int().min(0),
+ title: z.string(),
+ timestampMs: z.number().int().min(0),
+ maxPageNumber: z.number().int().min(1),
+ tags: z.array(z.string()),
+ breadLinks: z.array(z.string()).min(1),
+ })
+);
+const MAX_ITEM_LIMIT = process.env["AKIBA_SOUKEN_MAX_ITEM_LIMIT"];
+export class ArticleLoader {
+ constructor() { }
+ async loadData() {
+ const articleJsonPath = process.env["AKIBA_SOUKEN_ARTICLE_JSON"]!;
+ //console.log(`[${articleJsonPath}]`);
+ const jsonStr = await readFile(articleJsonPath, { encoding: "utf-8" }).then(text => {
+ const jsonObj = JSON.parse(text);
+ return jsonObj;
+ });
+ const parsedObj = zodType.parse(jsonStr);
+ parsedObj.sort((a, b) => {
+ return b.timestampMs - a.timestampMs;
+ });
+ if (MAX_ITEM_LIMIT != null) {
+ parsedObj.length = Math.min(Number(MAX_ITEM_LIMIT), parsedObj.length);
+ }
+ return parsedObj;
+ }
+ async getCategoryList() {
+ const loadedData = await this.loadData();
+ // key:カテゴリ名 , val:回数
+ const categoryCount = new Map<string, number>();
+ for (const a of loadedData) {
+ if (a.breadLinks.length == 0) {
+ continue;
+ }
+ const category = a.breadLinks[0];
+ if (categoryCount.has(category)) {
+ categoryCount.set(category, categoryCount.get(category)! + 1);
+ } else {
+ categoryCount.set(category, 1);
+ }
+ }
+ const categoryList: { name: string, count: number }[] = [];
+ for (const [name, count] of categoryCount) {
+ categoryList.push({ name: name, count });
+ }
+ categoryList.sort((a, b) => {
+ return b.count - a.count;
+ });
+ return categoryList;
+ }
+ async getTagList() {
+ const loadedData = await this.loadData();
+ // key:タグ名 , val:回数
+ const tagCount = new Map<string, number>();
+ for (const a of loadedData) {
+ const tags = new Set<string>();
+ a.tags.forEach(t => {
+ tags.add(t);
+ });
+ // パンくずリストの2件目以降はタグ扱いになっている
+ a.breadLinks.forEach((b, index) => {
+ if (index != 0) {
+ tags.add(b);
+ }
+ })
+ for (const tag of tags) {
+ if (tagCount.has(tag)) {
+ tagCount.set(tag, tagCount.get(tag)! + 1);
+ } else {
+ tagCount.set(tag, 1);
+ }
+ }
+ }
+ const tagList: { name: string, count: number }[] = [];
+ for (const [name, count] of tagCount) {
+ tagList.push({ name: name, count });
+ }
+ tagList.sort((a, b) => {
+ return b.count - a.count;
+ });
+ return tagList;
+ }
+}
diff --git a/src/util/pagenation.ts b/src/util/pagenation.ts
new file mode 100644
index 0000000..15ee01b
--- /dev/null
+++ b/src/util/pagenation.ts
@@ -0,0 +1,91 @@
+export type PagenationProp = {
+ now: number,
+ max: number,
+ between: number,
+}
+export function createPagenation(prop: PagenationProp) {
+ if (prop.between < 0) {
+ throw new Error(`betweenは0以上にして下さい`);
+ }
+ if (prop.max < 0) {
+ throw new Error(`maxは0以上にして下さい`);
+ }
+ if (prop.now < 0) {
+ throw new Error(`nowは0以上にして下さい`);
+ }
+ if (prop.max < prop.now) {
+ throw new Error(`nowの値はmaxと同じか、より小さな値にして下さい. now:${prop.now} , max:${prop.max}`);
+ }
+ if (prop.max == 0) {
+ return [];
+ }
+ const MIN = 1;
+ // 左・中・右の3ブロックの変数を作成
+ const blockLeft = MIN;
+ let blockMiddle = [
+ ...Array.from({ length: prop.between }).map((_, index) => {
+ const i = prop.now - prop.between + index;
+ return i;
+ }),
+ prop.now,
+ ...Array.from({ length: prop.between }).map((_, index) => {
+ const i = prop.now + index + 1;
+ return i;
+ }),
+ ];
+ blockMiddle = blockMiddle.filter(i => {
+ if (i <= blockLeft) {
+ return false;
+ }
+ if (prop.max <= i) {
+ return false;
+ }
+ return true;
+ });
+ const blockRight = prop.max;
+ type A = { type: "back", key: string, link: number | null };
+ type B = { type: "next", key: string, link: number | null };
+ type C = { type: "sp", key: string, };
+ type D = { type: "num", key: string, link: number | null, num: number }
+ // 結合を作成
+ const pageIdList: (A | B | C | D)[] = [];
+ {
+ // 左戻る矢印
+ if (prop.now == MIN) {
+ pageIdList.push({ type: "back", key: "back", link: null });
+ } else {
+ pageIdList.push({ type: "back", key: "back", link: prop.now - 1 });
+ }
+ // 最初のページ
+ pageIdList.push({ type: "num", key: `p-${blockLeft}`, link: blockLeft, num: blockLeft });
+ let lastPageId = blockLeft;
+ // 左と中の間の…を入れるかどうか
+ if (0 < blockMiddle.length && lastPageId + 1 != blockMiddle[0]) {
+ pageIdList.push({ type: "sp", key: "sp-left" });
+ }
+ blockMiddle.forEach(m => {
+ pageIdList.push({ type: "num", key: `p-${m}`, link: m, num: m });
+ lastPageId = m;
+ });
+ if (0 < blockMiddle.length && lastPageId + 1 != blockRight) {
+ // 最後のページ
+ pageIdList.push({ type: "sp", key: "sp-right" });
+ }
+ if (lastPageId != blockRight) {
+ pageIdList.push({ type: "num", key: `p-${blockRight}`, link: blockRight, num: blockRight });
+ lastPageId = blockRight;
+ }
+ // 右矢印
+ if (prop.now == prop.max) {
+ pageIdList.push({ type: "next", key: "next", link: null });
+ } else {
+ pageIdList.push({ type: "next", key: "next", link: prop.now + 1 });
+ }
+ pageIdList.forEach(p => {
+ if ("link" in p && p.link == prop.now) {
+ p.link = null;
+ }
+ })
+ }
+ return pageIdList
+} \ No newline at end of file