feat: create route to return title information

Summary:

Test Plan:
This commit is contained in:
2024-05-15 23:03:08 -04:00
parent 695a1bb4cd
commit 68c082493e
18 changed files with 1367 additions and 4 deletions

View File

@@ -13,3 +13,7 @@ pre-commit hook for Sapling (`.sl/config`):
[hooks] [hooks]
precommit = echo $HG_PARENT1 && bun prettier $(sl show -T "{file_adds} {file_mods}\n\n" $HG_PARENT1 --stat | head -n 1) --write precommit = echo $HG_PARENT1 && bun prettier $(sl show -T "{file_adds} {file_mods}\n\n" $HG_PARENT1 --stat | head -n 1) --write
``` ```
## Development
If a route is internal-only or doesn't need to appear on the OpenAPI spec (that's autogenerated by Hono), use the `Hono` class. Otherwise, use the `OpenAPIHono` class from `@hono/zod-openapi`.

BIN
bun.lockb

Binary file not shown.

View File

@@ -5,10 +5,13 @@
}, },
"dependencies": { "dependencies": {
"@hono/zod-openapi": "^0.12.0", "@hono/zod-openapi": "^0.12.0",
"gql.tada": "^1.7.4",
"graphql-request": "^7.0.1",
"hono": "^4.3.6", "hono": "^4.3.6",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@0no-co/graphqlsp": "^1.12.3",
"@cloudflare/workers-types": "^4.20240403.0", "@cloudflare/workers-types": "^4.20240403.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/bun": "^1.1.2", "@types/bun": "^1.1.2",

View File

@@ -7,6 +7,9 @@ const app = new OpenAPIHono();
const route = createRoute({ const route = createRoute({
method: "get", method: "get",
path: "/", path: "/",
summary: "Health check",
operationId: "healthCheck",
tags: ["aniplay"],
responses: { responses: {
200: { 200: {
content: { content: {
@@ -14,7 +17,7 @@ const route = createRoute({
schema: SuccessResponseSchema(), schema: SuccessResponseSchema(),
}, },
}, },
description: "Retrieve the user", description: "Server is up and running!",
}, },
}, },
}); });

View File

@@ -0,0 +1,76 @@
import { Title } from "~/types/title";
export async function fetchTitleFromAmvstrm(
aniListId: number,
): Promise<Title | undefined> {
return Promise.all([
fetch(`https://api-amvstrm.nyt92.eu.org/api/v2/info/${aniListId}`).then(
(res) => res.json() as Promise<any>,
),
fetchMissingInformationFromAnify(aniListId).catch((err) => {
console.error("Failed to get missing information from Anify", err);
return null;
}),
]).then(
async ([
{
id,
idMal,
title: { english: englishTitle, userPreferred: userPreferredTitle },
description,
episodes,
genres,
status,
bannerImage,
coverImage: {
extraLarge: extraLargeCoverImage,
large: largeCoverImage,
medium: mediumCoverImage,
},
countryOfOrigin,
nextair: nextAiringEpisode,
score: { averageScore },
},
anifyInfo,
]) => {
return {
id,
idMal,
title: {
userPreferred: userPreferredTitle,
english: englishTitle,
},
description,
episodes,
genres,
status,
averageScore,
bannerImage: bannerImage ?? anifyInfo?.bannerImage,
coverImage: {
extraLarge: extraLargeCoverImage,
large: largeCoverImage,
medium: mediumCoverImage,
},
countryOfOrigin: countryOfOrigin ?? anifyInfo?.countryOfOrigin,
nextAiringEpisode,
mediaListEntry: null,
};
},
);
}
type AnifyInformation = {
bannerImage: string | null;
countryOfOrigin: string;
};
function fetchMissingInformationFromAnify(
aniListId: number,
): Promise<AnifyInformation> {
return fetch(`https://api.anify.tv/info?id=${aniListId}`)
.then((res) => res.json() as Promise<AnifyInformation>)
.then(({ bannerImage, countryOfOrigin }) => ({
bannerImage,
countryOfOrigin,
}));
}

View File

@@ -0,0 +1,32 @@
import { graphql } from "gql.tada";
import { GraphQLClient } from "graphql-request";
import type { Title } from "~/types/title";
import { MediaFragment } from "./mediaFragment";
const GetTitleQuery = graphql(
`
query GetTitle($id: Int!) {
Media(id: $id) {
...Media
}
}
`,
[MediaFragment],
);
export async function fetchTitleFromAnilist(
id: number,
token: string | undefined,
): Promise<Title | undefined> {
const client = new GraphQLClient("https://graphql.anilist.co/");
const headers = new Headers();
if (token) {
headers.append("Authorization", `Bearer ${token}`);
}
return client
.request(GetTitleQuery, { id }, headers)
.then((data) => data?.Media ?? undefined);
}

View File

@@ -0,0 +1,127 @@
import { describe, expect, it } from "bun:test";
import app from "~/index";
import { server } from "~/mocks";
server.listen();
describe('requests the "/title" route', () => {
it("with a valid id & token", async () => {
const response = await app.request("/title?id=10", {
headers: new Headers({ "x-anilist-token": "asd" }),
});
expect(response.json()).resolves.toEqual({
success: true,
result: {
nextAiringEpisode: null,
mediaListEntry: {
status: "CURRENT",
progress: 1,
id: 402665918,
},
countryOfOrigin: "JP",
coverImage: {
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg",
extraLarge:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg",
},
averageScore: 66,
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg",
status: "FINISHED",
genres: ["Fantasy", "Thriller"],
episodes: 6,
description:
'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)',
title: {
userPreferred: "The Grimm Variations",
english: "The Grimm Variations",
},
idMal: 49210,
id: 135643,
},
});
expect(response.status).toBe(200);
});
it("with a valid id but no token", async () => {
const response = await app.request("/title?id=10");
expect(response.json()).resolves.toEqual({
success: true,
result: {
nextAiringEpisode: null,
mediaListEntry: null,
countryOfOrigin: "JP",
coverImage: {
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg",
extraLarge:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg",
},
averageScore: 66,
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg",
status: "FINISHED",
genres: ["Fantasy", "Thriller"],
episodes: 6,
description:
'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)',
title: {
userPreferred: "The Grimm Variations",
english: "The Grimm Variations",
},
idMal: 49210,
id: 135643,
},
});
expect(response.status).toBe(200);
});
it("with an unknown title from anilist but valid title from amvstrm", async () => {
const response = await app.request("/title?id=50");
expect(response.json()).resolves.toEqual({
success: true,
result: {
nextAiringEpisode: null,
mediaListEntry: null,
coverImage: {
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-yxY3olrjZH4k.png",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx151807-yxY3olrjZH4k.png",
},
averageScore: 83,
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/anime/banner/151807-37yfQA3ym8PA.jpg",
status: "FINISHED",
genres: ["Action", "Adventure", "Fantasy"],
episodes: 12,
description:
"They say whatever doesnt kill you makes you stronger, but thats not the case for the worlds weakest hunter Sung Jinwoo. After being brutally slaughtered by monsters in a high-ranking dungeon, Jinwoo came back with the System, a program only he could see, thats leveling him up in every way. Now, hes inspired to discover the secrets behind his powers and the dungeon that spawned them.<br>\n<br>\n(Source: Crunchyroll) <br><br>",
title: {
userPreferred: "Ore dake Level Up na Ken",
english: "Solo Leveling",
},
idMal: 52299,
id: 151807,
countryOfOrigin: "JP",
},
});
expect(response.status).toBe(200);
});
it("with an unknown title from all sources", async () => {
const response = await app.request("/title?id=-1");
expect(response.json()).resolves.toEqual({ success: false });
expect(response.status).toBe(404);
});
});

View File

@@ -0,0 +1,61 @@
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources";
import {
AniListIdSchema,
ErrorResponseSchema,
SuccessResponseSchema,
} from "~/types/schema";
import { Title } from "~/types/title";
import { fetchTitleFromAmvstrm } from "./amvstrm";
import { fetchTitleFromAnilist } from "./anilist";
const app = new OpenAPIHono();
const route = createRoute({
tags: ["aniplay", "title"],
operationId: "fetchTitle",
summary: "Fetch title information",
method: "get",
path: "/",
request: {
query: z.object({ id: AniListIdSchema }),
headers: z.object({ "x-anilist-token": z.string().nullish() }),
},
responses: {
200: {
content: {
"application/json": {
schema: SuccessResponseSchema(Title),
},
},
description: "Returns title information",
},
"404": {
content: {
"application/json": {
schema: ErrorResponseSchema,
},
},
description: "Title could not be found",
},
},
});
app.openapi(route, async (c) => {
const aniListId = Number(c.req.query("id"));
const aniListToken = c.req.header("X-AniList-Token");
const title = await fetchFromMultipleSources([
() => fetchTitleFromAnilist(aniListId, aniListToken ?? undefined),
() => fetchTitleFromAmvstrm(aniListId),
]);
if (!title) {
return c.json({ success: false }, 404);
}
return c.json({ success: true, result: title }, 200);
});
export default app;

View File

@@ -0,0 +1,35 @@
import { graphql } from "gql.tada";
export const MediaFragment = graphql(`
fragment Media on Media {
id
idMal
title {
english
userPreferred
}
type
description
episodes
genres
status
bannerImage
averageScore
coverImage {
extraLarge
large
medium
}
countryOfOrigin
mediaListEntry {
id
progress
status
}
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
`);

View File

@@ -8,6 +8,10 @@ app.route(
(controller) => controller.default, (controller) => controller.default,
), ),
); );
app.route(
"/title",
await import("~/controllers/title").then((controller) => controller.default),
);
// The OpenAPI documentation will be available at /doc // The OpenAPI documentation will be available at /doc
app.doc("/doc", { app.doc("/doc", {

602
src/mocks/amvstrm/title.ts Normal file
View File

@@ -0,0 +1,602 @@
import { HttpResponse, http } from "msw";
export function getAmvstrmTitle() {
return http.get(
"https://api-amvstrm.nyt92.eu.org/api/v2/info/:aniListId",
({ params }) => {
const aniListId = Number(params["aniListId"] as string);
if (aniListId == -1) {
return HttpResponse.json({
code: 404,
message:
"The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.",
});
}
if (aniListId == 50) {
return HttpResponse.json({
code: 200,
message: "success",
id: 151807,
idMal: 52299,
id_provider: {
idGogo: "ore-dake-level-up-na-ken",
idGogoDub: "ore-dake-level-up-na-ken-korean-dub",
idZoro: "solo-leveling-18718",
id9anime: "solo-leveling.3rpv2",
idPahe: "5421",
},
title: {
romaji: "Ore dake Level Up na Ken",
english: "Solo Leveling",
native: "俺だけレベルアップな件",
userPreferred: "Ore dake Level Up na Ken",
},
dub: true,
description:
"They say whatever doesnt kill you makes you stronger, but thats not the case for the worlds weakest hunter Sung Jinwoo. After being brutally slaughtered by monsters in a high-ranking dungeon, Jinwoo came back with the System, a program only he could see, thats leveling him up in every way. Now, hes inspired to discover the secrets behind his powers and the dungeon that spawned them.<br>\n<br>\n(Source: Crunchyroll) <br><br>",
coverImage: {
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx151807-yxY3olrjZH4k.png",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-yxY3olrjZH4k.png",
color: "#35bbf1",
},
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/anime/banner/151807-37yfQA3ym8PA.jpg",
genres: ["Action", "Adventure", "Fantasy"],
tags: [
{
id: 604,
name: "Dungeon",
},
{
id: 82,
name: "Male Protagonist",
},
{
id: 321,
name: "Urban Fantasy",
},
{
id: 66,
name: "Super Power",
},
{
id: 29,
name: "Magic",
},
{
id: 1243,
name: "Necromancy",
},
{
id: 326,
name: "Cultivation",
},
{
id: 111,
name: "War",
},
{
id: 104,
name: "Anti-Hero",
},
{
id: 94,
name: "Gore",
},
{
id: 636,
name: "Cosmic Horror",
},
{
id: 85,
name: "Tragedy",
},
{
id: 43,
name: "Swordplay",
},
{
id: 56,
name: "Shounen",
},
{
id: 146,
name: "Alternate Universe",
},
{
id: 96,
name: "Time Manipulation",
},
{
id: 1068,
name: "Angels",
},
{
id: 93,
name: "Post-Apocalyptic",
},
{
id: 217,
name: "Dystopian",
},
{
id: 1310,
name: "Travel",
},
{
id: 1045,
name: "Heterosexual",
},
{
id: 488,
name: "Age Regression",
},
{
id: 244,
name: "Isekai",
},
{
id: 171,
name: "Bullying",
},
{
id: 224,
name: "Dragons",
},
{
id: 255,
name: "Ninja",
},
],
status: "FINISHED",
format: "TV",
episodes: 12,
year: 2024,
season: "WINTER",
duration: 24,
startIn: {
year: 2024,
month: 1,
day: 7,
},
endIn: {
year: 2024,
month: 3,
day: 31,
},
nextair: null,
score: {
averageScore: 83,
decimalScore: 8.3,
},
popularity: 196143,
siteUrl: "https://anilist.co/anime/151807",
trailer: {
id: "HkIKAnwLZCw",
site: "youtube",
thumbnail: "https://i.ytimg.com/vi/HkIKAnwLZCw/hqdefault.jpg",
},
studios: [
{
name: "A-1 Pictures",
},
{
name: "Aniplex",
},
{
name: "Netmarble",
},
{
name: "D&C MEDIA",
},
{
name: "Kakao piccoma",
},
{
name: "Crunchyroll",
},
],
relation: [
{
id: 105398,
idMal: 121496,
title: {
romaji: "Na Honjaman Level Up",
english: "Solo Leveling",
native: "나 혼자만 레벨업",
userPreferred: "Na Honjaman Level Up",
},
coverImage: {
large:
"https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx105398-b673Vt5ZSuz3.jpg",
medium:
"https://s4.anilist.co/file/anilistcdn/media/manga/cover/small/bx105398-b673Vt5ZSuz3.jpg",
color: null,
},
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/manga/banner/105398-4UrEhdqZukrg.jpg",
genres: ["Action", "Adventure", "Fantasy"],
tags: [
{
id: 604,
name: "Dungeon",
},
{
id: 82,
name: "Male Protagonist",
},
{
id: 111,
name: "War",
},
{
id: 66,
name: "Super Power",
},
{
id: 207,
name: "Full Color",
},
{
id: 29,
name: "Magic",
},
{
id: 1243,
name: "Necromancy",
},
{
id: 321,
name: "Urban Fantasy",
},
{
id: 253,
name: "Gods",
},
{
id: 109,
name: "Primarily Adult Cast",
},
{
id: 103,
name: "Politics",
},
{
id: 93,
name: "Post-Apocalyptic",
},
{
id: 15,
name: "Demons",
},
{
id: 85,
name: "Tragedy",
},
{
id: 308,
name: "Video Games",
},
{
id: 365,
name: "Memory Manipulation",
},
{
id: 96,
name: "Time Manipulation",
},
{
id: 198,
name: "Foreign",
},
{
id: 1564,
name: "Estranged Family",
},
{
id: 171,
name: "Bullying",
},
{
id: 488,
name: "Age Regression",
},
{
id: 104,
name: "Anti-Hero",
},
{
id: 322,
name: "Assassins",
},
{
id: 774,
name: "Chimera",
},
{
id: 1045,
name: "Heterosexual",
},
{
id: 516,
name: "Language Barrier",
},
{
id: 153,
name: "Time Skip",
},
],
type: "MANGA",
format: "MANGA",
status: "FINISHED",
episodes: null,
duration: null,
averageScore: 85,
season: null,
},
{
id: 176496,
idMal: 58567,
title: {
romaji:
"Ore dake Level Up na Ken: Season 2 - Arise from the Shadow",
english: "Solo Leveling Season 2 -Arise from the Shadow-",
native:
"俺だけレベルアップな件 Season 2 -Arise from the Shadow-",
userPreferred:
"Ore dake Level Up na Ken: Season 2 - Arise from the Shadow",
},
coverImage: {
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx176496-r6oXxEqdZL0n.jpg",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx176496-r6oXxEqdZL0n.jpg",
color: "#a1bbe4",
},
bannerImage: null,
genres: ["Action", "Adventure", "Fantasy"],
tags: [
{
id: 1243,
name: "Necromancy",
},
{
id: 604,
name: "Dungeon",
},
],
type: "ANIME",
format: "TV",
status: "NOT_YET_RELEASED",
episodes: null,
duration: null,
averageScore: null,
season: null,
},
],
});
}
return HttpResponse.json({
code: 200,
message: "success",
id: 135643,
idMal: 49210,
id_provider: {
idGogo: "grimm-kumikyoku-dub",
idGogoDub: "grimm-kumikyoku",
idZoro: "the-grimm-variations-19092",
id9anime: "grimm-kumikyoku.qxvzn",
idPahe: "",
},
title: {
romaji: "Grimm Kumikyoku",
english: "The Grimm Variations",
native: "グリム組曲",
userPreferred: "Grimm Kumikyoku",
},
dub: true,
description:
'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)',
coverImage: {
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg",
color: "#fea150",
},
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg",
genres: ["Fantasy", "Thriller"],
tags: [
{
id: 400,
name: "Fairy Tale",
},
{
id: 193,
name: "Episodic",
},
{
id: 471,
name: "Anthology",
},
{
id: 227,
name: "Classic Literature",
},
{
id: 179,
name: "Witch",
},
{
id: 1219,
name: "Disability",
},
{
id: 93,
name: "Post-Apocalyptic",
},
{
id: 94,
name: "Gore",
},
{
id: 25,
name: "Historical",
},
{
id: 250,
name: "Rural",
},
{
id: 394,
name: "Writing",
},
{
id: 29,
name: "Magic",
},
{
id: 161,
name: "Bar",
},
{
id: 1578,
name: "Arranged Marriage",
},
{
id: 654,
name: "Denpa",
},
{
id: 217,
name: "Dystopian",
},
{
id: 598,
name: "Elf",
},
{
id: 456,
name: "Conspiracy",
},
{
id: 63,
name: "Space",
},
{
id: 364,
name: "Augmented Reality",
},
{
id: 112,
name: "Virtual World",
},
{
id: 639,
name: "Body Horror",
},
{
id: 163,
name: "Yandere",
},
{
id: 154,
name: "Body Swapping",
},
{
id: 100,
name: "Nudity",
},
],
status: "FINISHED",
format: "ONA",
episodes: 6,
year: 2024,
season: "SPRING",
duration: 44,
startIn: {
year: 2024,
month: 4,
day: 17,
},
endIn: {
year: 2024,
month: 4,
day: 17,
},
nextair: null,
score: {
averageScore: 66,
decimalScore: 6.6,
},
popularity: 8486,
siteUrl: "https://anilist.co/anime/135643",
trailer: {
id: "bTU3detmX_I",
site: "youtube",
thumbnail: "https://i.ytimg.com/vi/bTU3detmX_I/hqdefault.jpg",
},
studios: [
{
name: "Netflix",
},
{
name: "Wit Studio",
},
],
relation: [
{
id: 177039,
idMal: 169338,
title: {
romaji: "Grimm Kumikyoku",
english: null,
native: "グリム組曲",
userPreferred: "Grimm Kumikyoku",
},
coverImage: {
large:
"https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx177039-672FYniIpHIL.jpg",
medium:
"https://s4.anilist.co/file/anilistcdn/media/manga/cover/small/bx177039-672FYniIpHIL.jpg",
color: "#865028",
},
bannerImage: null,
genres: ["Fantasy", "Thriller"],
tags: [
{
id: 400,
name: "Fairy Tale",
},
{
id: 94,
name: "Gore",
},
{
id: 85,
name: "Tragedy",
},
{
id: 63,
name: "Space",
},
],
type: "MANGA",
format: "MANGA",
status: "RELEASING",
episodes: null,
duration: null,
averageScore: null,
season: null,
},
],
});
},
);
}

12
src/mocks/anify/title.ts Normal file
View File

@@ -0,0 +1,12 @@
import { HttpResponse, http } from "msw";
export function getAnifyTitle() {
return http.get(`https://api.anify.tv/info`, ({ request }) => {
// Construct a URL instance out of the intercepted request.
const url = new URL(request.url);
const id = url.searchParams.get("id");
// TODO: Actually return a response
return HttpResponse.json({ bannerImage: null, countryOfOrigin: "JP" });
});
}

View File

@@ -0,0 +1,70 @@
import { HttpResponse, graphql } from "msw";
export function getAnilistTitle() {
return graphql.query(
"GetTitle",
({ variables: { id }, request: { headers } }) => {
console.log(
`Intercepting GetTitle query with ID ${id} and Authorization header ${headers.get("authorization")}`,
);
if (id === -1 || id === 50) {
return HttpResponse.json({
errors: [
{
message: "Not Found.",
status: 404,
locations: [
{
line: 2,
column: 2,
},
],
},
],
data: {
Media: null,
},
});
}
return HttpResponse.json({
data: {
Media: {
id: 135643,
idMal: 49210,
title: {
english: "The Grimm Variations",
userPreferred: "The Grimm Variations",
},
description:
'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)',
episodes: 6,
genres: ["Fantasy", "Thriller"],
status: "FINISHED",
bannerImage:
"https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg",
averageScore: 66,
coverImage: {
extraLarge:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg",
large:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg",
medium:
"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg",
},
countryOfOrigin: "JP",
mediaListEntry: headers.has("authorization")
? {
id: 402665918,
progress: 1,
status: "CURRENT",
}
: null,
nextAiringEpisode: null,
},
},
});
},
);
}

View File

@@ -1 +1,5 @@
export const handlers = []; import { getAmvstrmTitle } from "./amvstrm/title";
import { getAnifyTitle } from "./anify/title";
import { getAnilistTitle } from "./anilist/title";
export const handlers = [getAnilistTitle(), getAmvstrmTitle(), getAnifyTitle()];

View File

@@ -8,3 +8,11 @@ export const SuccessResponseSchema = <T extends ZodSchema>(schema?: T) => {
return z.object({ success: z.literal(true), result: schema }); return z.object({ success: z.literal(true), result: schema });
}; };
export const ErrorResponseSchema = z.object({
success: z.literal(false),
});
export const AniListIdSchema = z
.number({ coerce: true })
.openapi({ type: "integer" });

View File

@@ -0,0 +1,253 @@
import { z } from "zod";
export const countryCodeSchema = z.enum([
"AD",
"AE",
"AF",
"AG",
"AI",
"AL",
"AM",
"AO",
"AQ",
"AR",
"AS",
"AT",
"AU",
"AW",
"AX",
"AZ",
"BA",
"BB",
"BD",
"BE",
"BF",
"BG",
"BH",
"BI",
"BJ",
"BL",
"BM",
"BN",
"BO",
"BQ",
"BR",
"BS",
"BT",
"BV",
"BW",
"BY",
"BZ",
"CA",
"CC",
"CD",
"CF",
"CG",
"CH",
"CI",
"CK",
"CL",
"CM",
"CN",
"CO",
"CR",
"CU",
"CV",
"CW",
"CX",
"CY",
"CZ",
"DE",
"DJ",
"DK",
"DM",
"DO",
"DZ",
"EC",
"EE",
"EG",
"EH",
"ER",
"ES",
"ET",
"FI",
"FJ",
"FK",
"FM",
"FO",
"FR",
"GA",
"GB",
"GD",
"GE",
"GF",
"GG",
"GH",
"GI",
"GL",
"GM",
"GN",
"GP",
"GQ",
"GR",
"GS",
"GT",
"GU",
"GW",
"GY",
"HK",
"HM",
"HN",
"HR",
"HT",
"HU",
"ID",
"IE",
"IL",
"IM",
"IN",
"IO",
"IQ",
"IR",
"IS",
"IT",
"JE",
"JM",
"JO",
"JP",
"KE",
"KG",
"KH",
"KI",
"KM",
"KN",
"KP",
"KR",
"KW",
"KY",
"KZ",
"LA",
"LB",
"LC",
"LI",
"LK",
"LR",
"LS",
"LT",
"LU",
"LV",
"LY",
"MA",
"MC",
"MD",
"ME",
"MF",
"MG",
"MH",
"MK",
"ML",
"MM",
"MN",
"MO",
"MP",
"MQ",
"MR",
"MS",
"MT",
"MU",
"MV",
"MW",
"MX",
"MY",
"MZ",
"NA",
"NC",
"NE",
"NF",
"NG",
"NI",
"NL",
"NO",
"NP",
"NR",
"NU",
"NZ",
"OM",
"PA",
"PE",
"PF",
"PG",
"PH",
"PK",
"PL",
"PM",
"PN",
"PR",
"PS",
"PT",
"PW",
"PY",
"QA",
"RE",
"RO",
"RS",
"RU",
"RW",
"SA",
"SB",
"SC",
"SD",
"SE",
"SG",
"SH",
"SI",
"SJ",
"SK",
"SL",
"SM",
"SN",
"SO",
"SR",
"SS",
"ST",
"SV",
"SX",
"SY",
"SZ",
"TC",
"TD",
"TF",
"TG",
"TH",
"TJ",
"TK",
"TL",
"TM",
"TN",
"TO",
"TR",
"TT",
"TV",
"TW",
"TZ",
"UA",
"UG",
"UM",
"US",
"UY",
"UZ",
"VA",
"VC",
"VE",
"VG",
"VI",
"VN",
"VU",
"WF",
"WS",
"YE",
"YT",
"ZA",
"ZM",
"ZW",
]);

60
src/types/title/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import { z } from "zod";
import { countryCodeSchema } from "./countryCodes";
export type Title = z.infer<typeof Title>;
export const Title = z.object({
nextAiringEpisode: z.nullable(
z.object({
episode: z.number(),
airingAt: z.number(),
timeUntilAiring: z.number(),
}),
),
mediaListEntry: z.nullable(
z.object({
status: z.nullable(
z.enum([
"CURRENT",
"PLANNING",
"COMPLETED",
"DROPPED",
"PAUSED",
"REPEATING",
]),
),
progress: z.number().nullable(),
id: z.number(),
}),
),
countryOfOrigin: z.optional(countryCodeSchema),
coverImage: z.nullable(
z.object({
medium: z.nullable(z.string()).optional(),
large: z.nullable(z.string()).optional(),
extraLarge: z.nullable(z.string()).optional(),
}),
),
averageScore: z.number().nullable(),
bannerImage: z.nullable(z.string()),
status: z.nullable(
z.enum([
"FINISHED",
"RELEASING",
"NOT_YET_RELEASED",
"CANCELLED",
"HIATUS",
]),
),
genres: z.nullable(z.array(z.nullable(z.string()))),
episodes: z.number().nullable(),
description: z.nullable(z.string()),
title: z.nullable(
z.object({
userPreferred: z.nullable(z.string()),
english: z.nullable(z.string()),
}),
),
idMal: z.number().nullable(),
id: z.number(),
});

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"types": ["@cloudflare/workers-types"], "types": ["@cloudflare/workers-types", "@types/bun"],
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"~/*": ["src/*"] "~/*": ["src/*"]
@@ -25,6 +25,15 @@
// Some stricter flags // Some stricter flags
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": false, "noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": true "noPropertyAccessFromIndexSignature": true,
// plugins
"plugins": [
{
"name": "@0no-co/graphqlsp",
"schema": "https://graphql.anilist.co",
"tadaOutputLocation": "./src/types/anilist-graphql.d.ts"
}
]
} }
} }