Skip to content
This repository was archived by the owner on Jun 2, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ dist/

# lefthook
lefthook-local.yml

src/data/*.json
74 changes: 74 additions & 0 deletions generate_10sen.ts
Original file line number Diff line number Diff line change
@@ -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)),
);
62 changes: 32 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
11 changes: 5 additions & 6 deletions src/components/Card.astro
Original file line number Diff line number Diff line change
@@ -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) : "#";
---

Expand All @@ -20,7 +19,7 @@ const furl = url ? new URL(url) : "#";
<div class="flex-shrink-0 relative z-1">
{<Image
class="w-full sm:w-[192px] h-full object-cover bg-black"
src={thumbnail as any}
src={(thumbnail || pseudoThumbnail) as any}
alt={title}
width={512}
height={288}
Expand All @@ -31,7 +30,7 @@ const furl = url ? new URL(url) : "#";
<div class="flex-grow relative flex-grow">
{<Image
class="absolute top-0 left-0 object-cover w-full h-full z-[-1] select-none group-hover:scale-105 transition-transform duration-500"
src={thumbnail as any}
src={(thumbnail || pseudoThumbnail) as any}
alt={title}
width={512}
height={288}
Expand Down
6 changes: 3 additions & 3 deletions src/components/TopSlides.astro
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
import { Image } from "astro:assets";
import type getThumbnail from "./getThumbnail";
import pseudoThumbnail from "../images/pseudo_thumbnail.jpg";

interface Props {
title: string;
href: string;
c: { title: string; thumbnail: Awaited<ReturnType<typeof getThumbnail>> }[];
c: { title: string; thumbnail: string | null }[];
}

const { title, href, c } = Astro.props;
Expand All @@ -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}
Expand Down
15 changes: 12 additions & 3 deletions src/components/getThumbnail.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ky from "ky";
import * as v from "valibot";
import pseudoThumbnail from "../images/pseudo_thumbnail.jpg";

Expand All @@ -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;
Expand Down
24 changes: 8 additions & 16 deletions src/pages/10sen/[year].astro
Original file line number Diff line number Diff line change
@@ -1,37 +1,29 @@
---
import NormalLayout from "../../layouts/Default.astro";
import fetch10sen from "../../data/fetch10sen";
import data from "../../data/10sen.json";
// biome-ignore lint/style/useImportType: <explanation>
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;

const a: [string, ComponentProps<typeof Card>[]][] = Object.entries(
Object.groupBy(
// biome-ignore lint/style/noNonNullAssertion: <explanation>
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: <explanation>
c!.map(({ count, ...rest }) => ({ ...rest })),
]);
---

<NormalLayout title={`音MAD作者が選ぶ${year}年の音MAD10選`}>
Expand Down
40 changes: 20 additions & 20 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,24 @@ import type { ComponentProps } from "astro/types";
// import data from "../data/10sen.json";
// biome-ignore lint/style/useImportType: <explanation>
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<typeof TopSlides>[] = 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<typeof TopSlides>[] = 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<typeof TopSlides>[] = await Promise.all(
Object.entries(await fetchMiru10())
.toReversed()
Expand All @@ -41,6 +38,7 @@ const miru10: ComponentProps<typeof TopSlides>[] = await Promise.all(
};
}),
);
*/
---


Expand All @@ -49,14 +47,16 @@ const miru10: ComponentProps<typeof TopSlides>[] = await Promise.all(
<p class="text-slate-900 mb-4 text-center">デバイス規制などの技術的な問題で、視聴可能な動画のサムネイルが取得出来ていないケースがありますので、あくまで参考程度にしてください。</p>
<div class="flex flex-col gap-y-8">
{
ten.map((props) => (
slides10sen.map((props) => (
<TopSlides {...props} />
))
}
{
/*
miru10.map((props) => (
<TopSlides {...props} />
))
*/
}
</div>
</NormalLayout>