diff --git a/.gitignore b/.gitignore index b95dc49..df163ad 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ dist/ # lefthook lefthook-local.yml + +src/data/*.json diff --git a/generate_10sen.ts b/generate_10sen.ts new file mode 100644 index 0000000..3b0334c --- /dev/null +++ b/generate_10sen.ts @@ -0,0 +1,74 @@ +import { writeFile } from "node:fs/promises"; +import ky from "ky"; +import * as v from "valibot"; + +const SOURCE_URL = "https://otodb.github.io/10sen-extract/data.json"; +const OUTPUT_PATH = new URL("./src/data/10sen.json", import.meta.url); + +const data10sen: Record< + string, + { count: number; url: string | null; title: string; type: string }[] +> = await ky.get(SOURCE_URL).json(); + +const resolved: { + year: string; + count: number; + title: string; + url: string | null; + thumbnail: string | null; +}[] = []; + +for (const [y, d] of Object.entries(data10sen)) { + console.log(`resolving: ${y} (${d.length} items)`); + for (const { count, title, url } of d) { + if (!url) { + resolved.push({ year: y, count, title, url, thumbnail: null }); + continue; + } + + const roxyUrl = new URL("https://roxy.otodb.net/json"); + roxyUrl.searchParams.set("q", url); + + try { + const roxyData = v.safeParse( + v.object({ + status: v.literal("ok"), + payload: v.object({ + title: v.string(), + url: v.string(), + thumbnail: v.string(), + }), + }), + await ky + .get(roxyUrl, { + retry: { limit: 3, methods: ["get"] }, + timeout: 20000, + throwHttpErrors: false, + }) + .json(), + ); + + if (!roxyData.success) { + resolved.push({ year: y, count, title, url, thumbnail: null }); + continue; + } + + resolved.push({ + year: y, + count, + url, + title: roxyData.output.payload.title, + thumbnail: roxyData.output.payload.thumbnail, + }); + console.log(`resolved: ${url}`); + } catch (e) { + console.error(e); + resolved.push({ year: y, count, title, url, thumbnail: null }); + } + } +} + +await writeFile( + OUTPUT_PATH, + JSON.stringify(Object.groupBy(resolved, (r) => r.year)), +); diff --git a/package.json b/package.json index be45f55..640c2d5 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,34 @@ { - "name": "hofs", - "type": "module", - "private": true, - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro build", - "preview": "wrangler pages dev ./dist", - "typecheck": "tsc --noEmit", - "check": "biome check .", - "fmt": "biome check --write ." - }, - "packageManager": "npm@10.9.2", - "dependencies": { - "@astrojs/svelte": "^7.0.0", - "@astrojs/tailwind": "^5.1.0", - "@fontsource-variable/jetbrains-mono": "^5.1.2", - "@fontsource/zen-antique-soft": "^5.1.1", - "@tailwindcss/container-queries": "^0.1.1", - "astro": "^5.0.0", - "ky": "^1.7.4", - "svelte": "^5.0.0", - "tailwindcss": "^3.4.1", - "valibot": "^1.0.0-beta.9" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "typescript": "^5.0.0", - "wrangler": "^3.0.0" - } + "name": "hofs", + "type": "module", + "private": true, + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "prebuild": "npm run generate:10sen", + "generate:10sen": "node --experimental-strip-types generate_10sen.ts", + "preview": "wrangler pages dev ./dist", + "typecheck": "tsc --noEmit", + "check": "biome check .", + "fmt": "biome check --write ." + }, + "packageManager": "npm@10.9.2", + "dependencies": { + "@astrojs/svelte": "^7.0.0", + "@astrojs/tailwind": "^5.1.0", + "@fontsource-variable/jetbrains-mono": "^5.1.2", + "@fontsource/zen-antique-soft": "^5.1.1", + "@tailwindcss/container-queries": "^0.1.1", + "astro": "^5.0.0", + "ky": "^1.7.4", + "svelte": "^5.0.0", + "tailwindcss": "^3.4.1", + "valibot": "^1.0.0-beta.9" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "typescript": "^5.0.0", + "wrangler": "^3.0.0" + } } diff --git a/src/components/Card.astro b/src/components/Card.astro index 1af1f3c..244d394 100644 --- a/src/components/Card.astro +++ b/src/components/Card.astro @@ -1,15 +1,14 @@ --- -import getThumbnail from "./getThumbnail"; +import pseudoThumbnail from "../images/pseudo_thumbnail.jpg"; import { Image } from "astro:assets"; interface Props { title: string; url: string | null; - type: string; + thumbnail?: string | null; } -const { title, url } = Astro.props; +const { title, url, thumbnail } = Astro.props; -const thumbnail = await getThumbnail(url); const furl = url ? new URL(url) : "#"; --- @@ -20,7 +19,7 @@ const furl = url ? new URL(url) : "#";
{{title} {{title}> }[]; + c: { title: string; thumbnail: string | null }[]; } const { title, href, c } = Astro.props; @@ -26,7 +26,7 @@ const { title, href, c } = Astro.props; left: (i - 0.125 * (i + 1)) * 384 + "px", zIndex: -i }} - src={thumbnail as any} + src={(thumbnail || pseudoThumbnail) as any} alt={title} width={512} height={288} diff --git a/src/components/getThumbnail.ts b/src/components/getThumbnail.ts index 103b029..4c6f95c 100644 --- a/src/components/getThumbnail.ts +++ b/src/components/getThumbnail.ts @@ -1,3 +1,4 @@ +import ky from "ky"; import * as v from "valibot"; import pseudoThumbnail from "../images/pseudo_thumbnail.jpg"; @@ -8,15 +9,23 @@ export default async function (url: string | null) { roxyUrl.searchParams.set("q", url); try { - const roxyRes = await fetch(roxyUrl); - if (roxyRes.status !== 200) return pseudoThumbnail; const roxyData = v.safeParse( v.object({ title: v.string(), url: v.string(), thumbnail: v.string(), }), - await roxyRes.json(), + await ky + .get(roxyUrl, { + retry: { + limit: 3, + methods: ["get"], + statusCodes: [408, 429, 500, 502, 503, 504], + backoffLimit: 3000, + }, + timeout: 10000, + }) + .json(), ); if (!roxyData.success) return pseudoThumbnail; diff --git a/src/pages/10sen/[year].astro b/src/pages/10sen/[year].astro index 489e2a4..d04f8f1 100644 --- a/src/pages/10sen/[year].astro +++ b/src/pages/10sen/[year].astro @@ -1,16 +1,12 @@ --- import NormalLayout from "../../layouts/Default.astro"; -import fetch10sen from "../../data/fetch10sen"; +import data from "../../data/10sen.json"; // biome-ignore lint/style/useImportType: import Card from "../../components/Card.astro"; import type { ComponentProps } from "astro/types"; -const data = await fetch10sen(); - export async function getStaticPaths() { - return Object.keys(await fetch10sen()).map((year) => ({ - params: { year }, - })); + return Object.keys(data).map((year) => ({ params: { year } })); } const { year } = Astro.params; @@ -18,20 +14,16 @@ const { year } = Astro.params; const a: [string, ComponentProps[]][] = Object.entries( Object.groupBy( // biome-ignore lint/style/noNonNullAssertion: - data[year]!, + data[year as keyof typeof data]!, ({ count }) => count, ), ) .toSorted(([a], [b]) => Number.parseInt(b, 10) - Number.parseInt(a, 10)) - .filter( - ( - x, - ): x is [ - string, - { count: number; url: string | null; title: string; type: string }[], - ] => !!x[1], - ) - .map(([b, c]) => [b, c.map(({ count, ...rest }) => ({ ...rest }))]); + .map(([b, c]) => [ + b, + // biome-ignore lint/style/noNonNullAssertion: + c!.map(({ count, ...rest }) => ({ ...rest })), + ]); --- diff --git a/src/pages/index.astro b/src/pages/index.astro index 83418b0..05497d5 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,27 +4,24 @@ import type { ComponentProps } from "astro/types"; // import data from "../data/10sen.json"; // biome-ignore lint/style/useImportType: import TopSlides from "../components/TopSlides.astro"; -import getThumbnail from "../components/getThumbnail"; -import fetch10sen from "../data/fetch10sen"; -import fetchMiru10 from "../data/fetchMiru10"; +import data10sen from "../data/10sen.json"; -const ten: ComponentProps[] = await Promise.all( - Object.entries(await fetch10sen()) - .toReversed() - .map(async ([year, values]) => { - return { - href: `/10sen/${year}`, - title: `音MAD作者が選ぶ${year}年の音MAD10選`, - c: await Promise.all( - values.slice(0, 8).map(async ({ title, url }) => ({ - title, - thumbnail: await getThumbnail(url), - })), - ), - }; - }), -); +const slides10sen: ComponentProps[] = Object.entries( + data10sen, +) + .toReversed() + .map(([year, values]) => { + return { + href: `/10sen/${year}`, + title: `音MAD作者が選ぶ${year}年の音MAD10選`, + c: values.slice(0, 8).map(({ title, thumbnail }) => ({ + title, + thumbnail: thumbnail, + })), + }; + }); +/* const miru10: ComponentProps[] = await Promise.all( Object.entries(await fetchMiru10()) .toReversed() @@ -41,6 +38,7 @@ const miru10: ComponentProps[] = await Promise.all( }; }), ); +*/ --- @@ -49,14 +47,16 @@ const miru10: ComponentProps[] = await Promise.all(

デバイス規制などの技術的な問題で、視聴可能な動画のサムネイルが取得出来ていないケースがありますので、あくまで参考程度にしてください。

{ - ten.map((props) => ( + slides10sen.map((props) => ( )) } { + /* miru10.map((props) => ( )) + */ }