From dbc78727bdcced853bedf89955dbcd9647ea810e Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Sat, 6 Dec 2025 10:00:26 -0500 Subject: [PATCH] refactor: cleaned up REST code also removed any references to Anify --- src/context.ts | 31 + src/controllers/auth/anilist/index.ts | 214 - src/controllers/auth/index.ts | 10 - .../__snapshots__/index.spec.ts.snap | 1072 ---- .../episodes/getByAniListId/anify.ts | 133 - .../episodes/getByAniListId/index.ts | 74 - .../episodes/getEpisodeUrl/anify.ts | 64 - .../episodes/getEpisodeUrl/index.ts | 146 - src/controllers/episodes/index.ts | 20 - .../episodes/markEpisodeAsWatched/index.ts | 103 - src/controllers/health-check/index.spec.ts | 11 - src/controllers/health-check/index.ts | 9 - src/controllers/internal/index.ts | 14 - .../internal/upcoming-titles/anilist.ts | 75 - .../internal/upcoming-titles/index.ts | 51 - src/controllers/popular/browse/index.ts | 56 - src/controllers/popular/category/index.ts | 67 - src/controllers/popular/index.ts | 15 - .../search/__snapshots__/index.spec.ts.snap | 4303 ----------------- src/controllers/search/index.spec.ts | 25 - src/controllers/search/index.ts | 84 - .../title/__snapshots__/index.spec.ts.snap | 703 --- src/controllers/title/index.spec.ts | 31 - src/controllers/title/index.ts | 73 - src/controllers/token/index.spec.ts | 175 - src/controllers/token/index.ts | 85 - src/controllers/watch-status/index.spec.ts | 203 - src/controllers/watch-status/index.ts | 139 - src/graphql.ts | 41 + src/graphql/context.ts | 26 - src/graphql/index.ts | 41 - src/graphql/resolvers/queries/home.ts | 36 - src/graphql/resolvers/queries/user.ts | 21 - src/index.ts | 4 +- .../index.ts => jobs/new-episode.ts} | 42 +- src/libs/anilist/anilist-do.ts | 2 +- .../maybeUpdateLastConnectedAt.ts | 0 src/mocks/anify/episodes.ts | 153 - src/mocks/anify/sources.ts | 46 - src/mocks/anify/title.ts | 12 - src/mocks/handlers.ts | 6 - src/mocks/mockData.ts | 4 +- src/models/schema.ts | 2 +- src/{graphql => }/resolvers/image.ts | 0 src/{graphql => }/resolvers/index.ts | 30 +- .../mutations/markEpisodeAsWatched.ts | 16 +- .../resolvers/mutations/updateToken.ts | 3 +- .../resolvers/mutations/updateWatchStatus.ts | 5 +- .../resolvers/queries/episodeStream.ts | 5 +- .../resolvers/queries/healthCheck.ts | 2 +- src/resolvers/queries/home.ts | 45 + .../resolvers/queries/popularBrowse.ts | 5 +- .../resolvers/queries/popularByCategory.ts | 7 +- src/{graphql => }/resolvers/queries/search.ts | 5 +- src/{graphql => }/resolvers/queries/title.ts | 3 +- src/resolvers/queries/user.ts | 22 + src/{graphql => }/resolvers/title.ts | 2 +- src/{graphql => }/schema.ts | 0 .../auth/anilist/getUser.ts | 0 .../episodes/getByAniListId/aniwatch.ts | 0 src/services/episodes/getByAniListId/index.ts | 19 + .../episodes/getEpisodeUrl/aniwatch.ts | 0 .../episodes/getEpisodeUrl/convertSkipTime.ts | 0 .../episodes/getEpisodeUrl/index.spec.ts | 0 src/services/episodes/getEpisodeUrl/index.ts | 50 + .../episodes/getEpisodeUrl/priorities.ts | 0 .../episodes/markEpisodeAsWatched/anilist.ts | 0 .../popular/browse/anilist.ts | 0 .../popular/category/anilist.ts | 0 .../popular/category/enum.ts | 0 .../popular/mapTitle.ts | 0 .../search/anilist.ts | 17 + .../watch-status/anilist.ts | 0 src/services/watch-status/index.ts | 22 + 74 files changed, 300 insertions(+), 8380 deletions(-) create mode 100644 src/context.ts delete mode 100644 src/controllers/auth/anilist/index.ts delete mode 100644 src/controllers/auth/index.ts delete mode 100644 src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap delete mode 100644 src/controllers/episodes/getByAniListId/anify.ts delete mode 100644 src/controllers/episodes/getByAniListId/index.ts delete mode 100644 src/controllers/episodes/getEpisodeUrl/anify.ts delete mode 100644 src/controllers/episodes/getEpisodeUrl/index.ts delete mode 100644 src/controllers/episodes/index.ts delete mode 100644 src/controllers/episodes/markEpisodeAsWatched/index.ts delete mode 100644 src/controllers/health-check/index.spec.ts delete mode 100644 src/controllers/health-check/index.ts delete mode 100644 src/controllers/internal/index.ts delete mode 100644 src/controllers/internal/upcoming-titles/anilist.ts delete mode 100644 src/controllers/internal/upcoming-titles/index.ts delete mode 100644 src/controllers/popular/browse/index.ts delete mode 100644 src/controllers/popular/category/index.ts delete mode 100644 src/controllers/popular/index.ts delete mode 100644 src/controllers/search/__snapshots__/index.spec.ts.snap delete mode 100644 src/controllers/search/index.spec.ts delete mode 100644 src/controllers/search/index.ts delete mode 100644 src/controllers/title/__snapshots__/index.spec.ts.snap delete mode 100644 src/controllers/title/index.spec.ts delete mode 100644 src/controllers/title/index.ts delete mode 100644 src/controllers/token/index.spec.ts delete mode 100644 src/controllers/token/index.ts delete mode 100644 src/controllers/watch-status/index.spec.ts delete mode 100644 src/controllers/watch-status/index.ts create mode 100644 src/graphql.ts delete mode 100644 src/graphql/context.ts delete mode 100644 src/graphql/index.ts delete mode 100644 src/graphql/resolvers/queries/home.ts delete mode 100644 src/graphql/resolvers/queries/user.ts rename src/{controllers/internal/new-episode/index.ts => jobs/new-episode.ts} (64%) rename src/{controllers => middleware}/maybeUpdateLastConnectedAt.ts (100%) delete mode 100644 src/mocks/anify/episodes.ts delete mode 100644 src/mocks/anify/sources.ts delete mode 100644 src/mocks/anify/title.ts rename src/{graphql => }/resolvers/image.ts (100%) rename src/{graphql => }/resolvers/index.ts (64%) rename src/{graphql => }/resolvers/mutations/markEpisodeAsWatched.ts (70%) rename src/{graphql => }/resolvers/mutations/updateToken.ts (92%) rename src/{graphql => }/resolvers/mutations/updateWatchStatus.ts (88%) rename src/{graphql => }/resolvers/queries/episodeStream.ts (73%) rename src/{graphql => }/resolvers/queries/healthCheck.ts (70%) create mode 100644 src/resolvers/queries/home.ts rename src/{graphql => }/resolvers/queries/popularBrowse.ts (80%) rename src/{graphql => }/resolvers/queries/popularByCategory.ts (75%) rename src/{graphql => }/resolvers/queries/search.ts (79%) rename src/{graphql => }/resolvers/queries/title.ts (92%) create mode 100644 src/resolvers/queries/user.ts rename src/{graphql => }/resolvers/title.ts (84%) rename src/{graphql => }/schema.ts (100%) rename src/{controllers => services}/auth/anilist/getUser.ts (100%) rename src/{controllers => services}/episodes/getByAniListId/aniwatch.ts (100%) create mode 100644 src/services/episodes/getByAniListId/index.ts rename src/{controllers => services}/episodes/getEpisodeUrl/aniwatch.ts (100%) rename src/{controllers => services}/episodes/getEpisodeUrl/convertSkipTime.ts (100%) rename src/{controllers => services}/episodes/getEpisodeUrl/index.spec.ts (100%) create mode 100644 src/services/episodes/getEpisodeUrl/index.ts rename src/{controllers => services}/episodes/getEpisodeUrl/priorities.ts (100%) rename src/{controllers => services}/episodes/markEpisodeAsWatched/anilist.ts (100%) rename src/{controllers => services}/popular/browse/anilist.ts (100%) rename src/{controllers => services}/popular/category/anilist.ts (100%) rename src/{controllers => services}/popular/category/enum.ts (100%) rename src/{controllers => services}/popular/mapTitle.ts (100%) rename src/{controllers => services}/search/anilist.ts (68%) rename src/{controllers => services}/watch-status/anilist.ts (100%) create mode 100644 src/services/watch-status/index.ts diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..f6e1c42 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,31 @@ +import type { Context as HonoContext } from "hono"; + +export interface GraphQLContext { + db: D1Database; + deviceId?: string; + aniListToken?: string; + user: { id: number; name: string } | null; + honoContext: HonoContext; +} + +export async function createGraphQLContext( + c: HonoContext, +): Promise { + const deviceId = c.req.header("X-Device-ID"); + const aniListToken = c.req.header("X-AniList-Token"); + const env = c.env as Env; + + let user: GraphQLContext["user"] = null; + if (aniListToken) { + const stub = await env.ANILIST_DO.getByName("GLOBAL"); + user = await stub.getUser(aniListToken!); + } + + return { + db: env.DB, + deviceId, + aniListToken, + user, + honoContext: c, + }; +} diff --git a/src/controllers/auth/anilist/index.ts b/src/controllers/auth/anilist/index.ts deleted file mode 100644 index 8c7ebe2..0000000 --- a/src/controllers/auth/anilist/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import { env } from "cloudflare:workers"; -import { streamSSE } from "hono/streaming"; - -import { fetchEpisodes } from "~/controllers/episodes/getByAniListId"; -import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; -import { associateDeviceIdWithUsername } from "~/models/token"; -import { setWatchStatus } from "~/models/watchStatus"; -import { EpisodesResponseSchema } from "~/types/episode"; -import { ErrorResponse, ErrorResponseSchema } from "~/types/schema"; -import { Title } from "~/types/title"; - -import { getUser } from "./getUser"; - -const UserSchema = z.object({ - name: z.string(), - avatar: z.object({ - medium: z.string().nullable(), - large: z.string(), - }), - statistics: z.object({ - minutesWatched: z.number().openapi({ type: "integer", format: "int64" }), - episodesWatched: z.number().int(), - count: z.number().int(), - meanScore: z.number().openapi({ type: "number", format: "float" }), - }), -}); - -const route = createRoute({ - tags: ["aniplay", "auth"], - summary: - "Authenticate with AniList and return all upcoming and 'currently watching' titles", - operationId: "authenticateAniList", - method: "get", - path: "/", - request: { - headers: z.object({ - "x-anilist-token": z.string(), - "x-aniplay-device-id": z.string(), - }), - // Uncomment when testing locally - // headers: z.object({ - // "x-anilist-token": - // process.env.NODE_ENV === "production" - // ? z.string() - // : z.string().optional(), - // "x-aniplay-device-id": - // process.env.NODE_ENV === "production" - // ? z.string() - // : z.string().optional(), - // }), - // query: z.object({ - // aniListToken: z.string().optional(), - // deviceId: z.string().optional(), - // }), - }, - responses: { - 200: { - content: { - "text/event-stream": { - schema: z.union([ - z.object({ title: Title, episodes: EpisodesResponseSchema }), - UserSchema, - ]), - }, - }, - description: "Streams a list of titles", - }, - 401: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Failed to authenticate with AniList", - }, - 500: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Error fetching episodes", - }, - }, -}); - -const app = new OpenAPIHono(); - -app.openapi(route, async (c) => { - const deviceId = - c.req.header("X-Aniplay-Device-Id") ?? c.req.query("deviceId"); - const aniListToken = - c.req.header("X-AniList-Token") ?? c.req.query("aniListToken"); - - if (!aniListToken) { - return c.json(ErrorResponse, { status: 401 }); - } - - let user: Awaited>; - try { - user = await getUser(aniListToken); - if (!user) { - return c.json(ErrorResponse, { status: 401 }); - } - } catch (error) { - console.error("Failed to authenticate with AniList"); - console.error(error); - return c.json(ErrorResponse, { status: 500 }); - } - - try { - await associateDeviceIdWithUsername(deviceId!, user.name!); - } catch (error) { - console.error("Failed to associate device"); - console.error(error); - return c.json(ErrorResponse, { status: 500 }); - } - - c.header("Content-Type", "text/x-unknown"); - c.header("content-encoding", "identity"); - c.header("transfer-encoding", "chunked"); - return streamSSE( - c, - async (stream) => { - await stream.writeSSE({ event: "user", data: JSON.stringify(user) }); - - let currentPage = 1; - let hasNextPage = true; - - do { - const stub = env.ANILIST_DO.getByName(user.name!); - const { mediaList, pageInfo } = await stub - .getTitles( - user.name!, - currentPage++, - ["CURRENT", "PLANNING", "PAUSED", "REPEATING"], - aniListToken, - ) - .then((data) => data!); - if (!mediaList) { - break; - } - - if (!(pageInfo?.hasNextPage ?? false) && (pageInfo?.total ?? 0) > 0) { - await stream.writeSSE({ - event: "count", - data: pageInfo!.total.toString(), - }); - } - - for (const mediaObj of mediaList) { - const media = mediaObj?.media; - if (!media) { - continue; - } - - const mediaListEntry = media.mediaListEntry; - if (mediaListEntry) { - const { wasAdded } = await setWatchStatus( - deviceId!, - media.id, - mediaListEntry.status, - ); - if (wasAdded) { - await maybeScheduleNextAiringEpisode(media.id); - } - } - - const nextEpisode = media.nextAiringEpisode?.episode; - if ( - nextEpisode === 0 || - nextEpisode === 1 || - media.status === "NOT_YET_RELEASED" - ) { - await stream.writeSSE({ - event: "title", - data: JSON.stringify({ title: media, episodes: [] }), - id: media.id.toString(), - }); - continue; - } - - await fetchEpisodes(media.id, true).then((episodes) => { - if (episodes.length === 0) { - return; - } - - return stream.writeSSE({ - event: "title", - data: JSON.stringify({ title: media, episodes }), - id: media.id.toString(), - }); - }); - } - - hasNextPage = pageInfo?.hasNextPage ?? false; - console.log(hasNextPage); - } while (hasNextPage); - - // send end event instead of closing the connection to let the client know that the stream didn't end abruptly - await stream.writeSSE({ event: "end", data: "end" }); - console.log("completed"); - }, - async (err, stream) => { - console.error("Error occurred in SSE"); - console.error(err); - await stream.writeln("An error occurred"); - await stream.close(); - }, - ); -}); - -export default app; diff --git a/src/controllers/auth/index.ts b/src/controllers/auth/index.ts deleted file mode 100644 index 939f32d..0000000 --- a/src/controllers/auth/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { OpenAPIHono } from "@hono/zod-openapi"; - -const app = new OpenAPIHono(); - -app.route( - "/anilist", - await import("./anilist").then((controller) => controller.default), -); - -export default app; diff --git a/src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap b/src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap deleted file mode 100644 index 284d76d..0000000 --- a/src/controllers/episodes/getByAniListId/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,1072 +0,0 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; - -exports[`requests the "/episodes" route with list of episodes from Anify 1`] = ` -{ - "result": { - "episodes": [ - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103233", - "img": null, - "number": 1, - "rating": null, - "title": "Mission: Forgetter I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=103632", - "img": null, - "number": 2, - "rating": null, - "title": "Mission: Forgetter II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104244", - "img": null, - "number": 3, - "rating": null, - "title": "Mission: Forgetter III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104620", - "img": null, - "number": 4, - "rating": null, - "title": "Mission: Forgetter IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=104844", - "img": null, - "number": 5, - "rating": null, - "title": "File: Glint", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=105761", - "img": null, - "number": 6, - "rating": null, - "title": "File: Dreamspeaker Thea", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106135", - "img": null, - "number": 7, - "rating": null, - "title": "File: Forgetter Annette", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106518", - "img": null, - "number": 8, - "rating": null, - "title": "Mission: Dreamspeaker I", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106606", - "img": null, - "number": 9, - "rating": null, - "title": "Mission: Dreamspeaker II", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=106981", - "img": null, - "number": 10, - "rating": null, - "title": "Mission: Dreamspeaker III", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107176", - "img": null, - "number": 11, - "rating": null, - "title": "Mission: Dreamspeaker IV", - "updatedAt": 0, - }, - { - "description": null, - "id": "/watch/spy-classroom-season-2-18468?ep=107247", - "img": null, - "number": 12, - "rating": null, - "title": "File: Flower Garden Lily", - "updatedAt": 0, - }, - ], - "providerId": "zoro", - }, - "success": true, -} -`; diff --git a/src/controllers/episodes/getByAniListId/anify.ts b/src/controllers/episodes/getByAniListId/anify.ts deleted file mode 100644 index 4752147..0000000 --- a/src/controllers/episodes/getByAniListId/anify.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { DateTime } from "luxon"; - -import { PromiseTimedOutError, promiseTimeout } from "~/libs/promiseTimeout"; -import { readEnvVariable } from "~/libs/readEnvVariable"; -import { sortByProperty } from "~/libs/sortByProperty"; -import { getValue, setValue } from "~/models/kv"; -import type { EpisodesResponse } from "~/types/episode"; - -export async function getEpisodesFromAnify( - aniListId: number, -): Promise { - if (await shouldSkipAnify(aniListId)) { - console.log("Skipping Anify for title", aniListId); - return null; - } - - let response: AnifyEpisodesResponse[] | null = null; - const abortController = new AbortController(); - try { - response = await promiseTimeout( - fetch(`https://anify.eltik.cc/episodes/${aniListId}`, { - signal: abortController.signal, - }).then((res) => res.json() as Promise), - 30 * 1000, - ); - if ("error" in response) { - const error = response.error; - if (error === "Too many requests") { - console.log( - "Sending too many requests to Anify, setting killswitch until", - DateTime.now().plus({ minutes: 1 }).toISO(), - ); - setValue( - "anify_killswitch_till", - DateTime.now().plus({ minutes: 1 }).toISO(), - ); - } - - return null; - } - } catch (e) { - if (e instanceof PromiseTimedOutError) { - abortController.abort("Loading episodes from Anify timed out"); - } - console.error( - `Error trying to load episodes from anify; aniListId: ${aniListId}`, - ); - console.error(e); - } - - if (!response || response.length === 0) { - return null; - } - - const sourcePriority = { - zoro: 1, - gogoanime: 2, - }; - const filteredEpisodesData = response - .filter(({ providerId }) => { - if (providerId === "9anime" || providerId === "animepahe") { - return false; - } - if (aniListId == 166873 && providerId === "zoro") { - // Mushoku Tensei: Job Reincarnation S2 Part 2 returns incorrect mapping for Zoro only - return false; - } - - return true; - }) - .sort(sortByProperty(sourcePriority, "providerId")); - - if (filteredEpisodesData.length === 0) { - return null; - } - const selectedEpisodeData = filteredEpisodesData[0]; - return { - providerId: selectedEpisodeData.providerId, - episodes: selectedEpisodeData.episodes.map( - ({ id, number, description, img, rating, title, updatedAt }) => ({ - id, - number, - description, - img, - rating, - title, - updatedAt: updatedAt ?? 0, - }), - ), - }; -} - -export async function shouldSkipAnify(aniListId: number): Promise { - if (!readEnvVariable("ENABLE_ANIFY")) { - return true; - } - - // Some mappings on Anify are incorrect so they return episodes from a similar title - if ( - [ - 153406, // Tower of God S2 - 158927, // Spy x Family S2 - 166873, // Mushoku Tensei: Jobless Reincarnation S2 part 2 - 163134, // Re:ZERO -Starting Life in Another World- Season 3 - 163146, // Blue Lock S2 - ].includes(aniListId) - ) { - return true; - } - - return await getValue("anify_killswitch_till").then((dateTime) => { - if (!dateTime) { - return false; - } - - return DateTime.fromISO(dateTime).diffNow().as("minutes") > 0; - }); -} - -interface AnifyEpisodesResponse { - providerId: string; - episodes: { - id: string; - isFiller: boolean | undefined; - number: number; - title: string; - img: string | null; - hasDub: boolean; - description: string | null; - rating: number | null; - updatedAt: number | undefined; - }[]; -} diff --git a/src/controllers/episodes/getByAniListId/index.ts b/src/controllers/episodes/getByAniListId/index.ts deleted file mode 100644 index dbbd00e..0000000 --- a/src/controllers/episodes/getByAniListId/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; - -import { EpisodesResponseSchema } from "~/types/episode"; -import { - AniListIdQuerySchema, - ErrorResponse, - ErrorResponseSchema, -} from "~/types/schema"; - -const route = createRoute({ - tags: ["aniplay", "episodes"], - summary: "Fetch episodes for a title", - operationId: "fetchEpisodes", - method: "get", - path: "/{aniListId}", - request: { - params: z.object({ aniListId: AniListIdQuerySchema }), - }, - responses: { - 200: { - content: { - "application/json": { - schema: EpisodesResponseSchema, - }, - }, - description: "Returns a list of episodes", - }, - 500: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Error fetching episodes", - }, - }, -}); - -const app = new OpenAPIHono(); - -export function fetchEpisodes(aniListId: number, shouldRetry: boolean = false) { - return import("./aniwatch") - .then(({ getEpisodesFromAniwatch }) => - getEpisodesFromAniwatch(aniListId, shouldRetry), - ) - .then((episodeResults) => episodeResults?.episodes ?? []); -} - -app.openapi(route, async (c) => { - const aniListId = Number(c.req.param("aniListId")); - - // Check if we should use mock data - const { useMockData } = await import("~/libs/useMockData"); - if (useMockData()) { - const { mockEpisodes } = await import("~/mocks/mockData"); - - return c.json({ - success: true, - result: { providerId: "aniwatch", episodes: mockEpisodes() }, - }); - } - - const episodes = await fetchEpisodes(aniListId); - if (episodes.length === 0) { - return c.json(ErrorResponse, { status: 404 }); - } - - return c.json({ - success: true, - result: { providerId: "aniwatch", episodes }, - }); -}); - -export default app; diff --git a/src/controllers/episodes/getEpisodeUrl/anify.ts b/src/controllers/episodes/getEpisodeUrl/anify.ts deleted file mode 100644 index 7815c19..0000000 --- a/src/controllers/episodes/getEpisodeUrl/anify.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { sortByProperty } from "~/libs/sortByProperty"; -import type { FetchUrlResponse } from "~/types/episode/fetch-url-response"; - -import { type SkipTime, convertSkipTime } from "./convertSkipTime"; -import { - audioPriority, - qualityPriority, - subtitlesPriority, -} from "./priorities"; - -export async function getSourcesFromAnify( - provider: string, - watchId: string, - aniListId: number, -): Promise { - const response = await fetch("https://anify.eltik.cc/sources", { - body: JSON.stringify({ - watchId, - providerId: provider, - episodeNumber: "1", - id: aniListId.toString(), - subType: "sub", - }), - method: "POST", - }).then((res) => res.json() as Promise); - const { sources, subtitles, audio, intro, outro, headers } = response; - - if (!sources || sources.length === 0) { - return null; - } - - const source = sources.sort(sortByProperty(qualityPriority, "quality"))[0] - ?.url; - subtitles?.sort(sortByProperty(subtitlesPriority, "lang")); - audio?.sort(sortByProperty(audioPriority, "lang")); - - return { - source, - audio, - subtitles, - intro: convertSkipTime(intro), - outro: convertSkipTime(outro), - headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined, - }; -} - -interface AnifySourcesResponse { - sources: VideoSource[]; - subtitles: LanguageSource[]; - audio: LanguageSource[]; - intro: SkipTime; - outro: SkipTime; - headers?: Record; -} - -interface VideoSource { - url: string; - quality: string; -} - -interface LanguageSource { - url: string; - lang: string; -} diff --git a/src/controllers/episodes/getEpisodeUrl/index.ts b/src/controllers/episodes/getEpisodeUrl/index.ts deleted file mode 100644 index f1c90d9..0000000 --- a/src/controllers/episodes/getEpisodeUrl/index.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; - -import { FetchUrlResponse } from "~/types/episode/fetch-url-response"; -import { - AniListIdQuerySchema, - EpisodeNumberSchema, - ErrorResponse, - ErrorResponseSchema, -} from "~/types/schema"; - -import { fetchEpisodes } from "../getByAniListId"; - -const FetchUrlRequest = z.object({ episodeNumber: EpisodeNumberSchema }); - -const route = createRoute({ - tags: ["aniplay", "episodes"], - summary: "Fetch stream URL for an episode", - operationId: "fetchStreamUrl", - method: "post", - path: "/{aniListId}/url", - request: { - params: z.object({ aniListId: AniListIdQuerySchema }), - body: { - content: { - "application/json": { - schema: FetchUrlRequest, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: FetchUrlResponse, - }, - }, - description: "Returns a stream URL", - }, - 400: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Unknown provider", - }, - 404: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Provider did not return a source", - }, - 500: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Failed to fetch stream URL from provider", - }, - }, -}); - -const app = new OpenAPIHono(); - -export async function fetchEpisodeUrl({ - id, - aniListId, - episodeNumber, -}: - | { id: string; aniListId?: number; episodeNumber?: number } - | { - id?: string; - aniListId: number; - episodeNumber: number; - }): Promise { - try { - let episodeId = id; - if (!id) { - const episodes = await fetchEpisodes(aniListId!); - if (episodes.length === 0) { - console.error(`Failed to fetch episodes for title ${aniListId}`); - return null; - } - const episode = episodes.find( - (episode) => episode.number === episodeNumber, - ); - if (!episode) { - console.error( - `Episode ${episodeNumber} not found for title ${aniListId}`, - ); - return null; - } - - episodeId = episode.id; - } - - const result = await import("./aniwatch").then( - ({ getSourcesFromAniwatch }) => getSourcesFromAniwatch(episodeId!), - ); - if (!result) { - return null; - } - - return result; - } catch (e) { - console.error("Failed to fetch download URL from Aniwatch", e); - - throw e; - } -} - -app.openapi(route, async (c) => { - const aniListId = Number(c.req.param("aniListId")); - const { episodeNumber } = await c.req.json(); - if (episodeNumber == undefined) { - return c.json(ErrorResponse, { status: 400 }); - } - - // Check if we should use mock data - const { useMockData } = await import("~/libs/useMockData"); - if (useMockData()) { - const { mockEpisodeUrl } = await import("~/mocks/mockData"); - - return c.json({ success: true, result: mockEpisodeUrl }); - } - - try { - console.log( - `Fetching episode URL for aniListId: ${aniListId}, episodeNumber: ${episodeNumber}`, - ); - const fetchUrlResult = await fetchEpisodeUrl({ aniListId, episodeNumber }); - if (!fetchUrlResult) { - return c.json(ErrorResponse, { status: 404 }); - } - - return c.json({ success: true, result: fetchUrlResult }); - } catch (error) { - return c.json(ErrorResponse, { status: 500 }); - } -}); - -export default app; diff --git a/src/controllers/episodes/index.ts b/src/controllers/episodes/index.ts deleted file mode 100644 index f373810..0000000 --- a/src/controllers/episodes/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OpenAPIHono } from "@hono/zod-openapi"; - -const app = new OpenAPIHono(); - -app.route( - "/", - await import("./getByAniListId").then((controller) => controller.default), -); -app.route( - "/", - await import("./getEpisodeUrl").then((controller) => controller.default), -); -app.route( - "/", - await import("./markEpisodeAsWatched").then( - (controller) => controller.default, - ), -); - -export default app; diff --git a/src/controllers/episodes/markEpisodeAsWatched/index.ts b/src/controllers/episodes/markEpisodeAsWatched/index.ts deleted file mode 100644 index ecf19ac..0000000 --- a/src/controllers/episodes/markEpisodeAsWatched/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import { env } from "hono/adapter"; - -import { updateWatchStatus } from "~/controllers/watch-status"; -import { - AniListIdQuerySchema, - EpisodeNumberSchema, - ErrorResponse, - ErrorResponseSchema, - SuccessResponseSchema, -} from "~/types/schema"; -import { User } from "~/types/user"; - -import { markEpisodeAsWatched } from "./anilist"; - -const MarkEpisodeAsWatchedRequest = z.object({ - episodeNumber: EpisodeNumberSchema, - isComplete: z.boolean(), -}); - -const route = createRoute({ - tags: ["aniplay", "episodes"], - summary: "Mark episode as watched", - operationId: "markEpisodeAsWatched", - method: "post", - path: "/{aniListId}/watched", - request: { - params: z.object({ aniListId: AniListIdQuerySchema }), - body: { - content: { - "application/json": { - schema: MarkEpisodeAsWatchedRequest, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: SuccessResponseSchema(User), - }, - }, - description: "Returns whether the episode was marked as watched", - }, - 401: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Unauthorized to mark the episode as watched", - }, - 500: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Error marking episode as watched", - }, - }, -}); - -const app = new OpenAPIHono(); - -app.openapi(route, async (c) => { - const aniListToken = c.req.header("X-AniList-Token"); - - if (!aniListToken) { - return c.json(ErrorResponse, { status: 401 }); - } - - const deviceId = c.req.header("X-Aniplay-Device-Id")!; - const aniListId = Number(c.req.param("aniListId")); - const { episodeNumber, isComplete } = - await c.req.json(); - - try { - const user = await markEpisodeAsWatched( - aniListToken, - aniListId, - episodeNumber, - isComplete, - ); - if (isComplete) { - await updateWatchStatus(deviceId, aniListId, "COMPLETED"); - } - - if (!user) { - console.error("Failed to mark episode as watched - user not found?"); - return c.json(ErrorResponse, { status: 500 }); - } - - return c.json({ success: true, result: user }, 200); - } catch (error) { - console.error("Failed to mark episode as watched"); - console.error(error); - return c.json(ErrorResponse, { status: 500 }); - } -}); - -export default app; diff --git a/src/controllers/health-check/index.spec.ts b/src/controllers/health-check/index.spec.ts deleted file mode 100644 index 921ca43..0000000 --- a/src/controllers/health-check/index.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, expect, it } from "bun:test"; - -import app from "~/index"; - -describe("Health Check", () => { - it("should return { success: true }", async () => { - const res = await app.request("/"); - - expect(res.json()).resolves.toEqual({ success: true }); - }); -}); diff --git a/src/controllers/health-check/index.ts b/src/controllers/health-check/index.ts deleted file mode 100644 index 939b549..0000000 --- a/src/controllers/health-check/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Hono } from "hono"; - -import { SuccessResponse } from "~/types/schema"; - -const app = new Hono(); - -app.get("/", (c) => c.json(SuccessResponse, 200)); - -export default app; diff --git a/src/controllers/internal/index.ts b/src/controllers/internal/index.ts deleted file mode 100644 index 2b32af9..0000000 --- a/src/controllers/internal/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Hono } from "hono"; - -const app = new Hono(); - -app.route( - "/new-episode", - await import("./new-episode").then((controller) => controller.default), -); -app.route( - "/upcoming-titles", - await import("./upcoming-titles").then((controller) => controller.default), -); - -export default app; diff --git a/src/controllers/internal/upcoming-titles/anilist.ts b/src/controllers/internal/upcoming-titles/anilist.ts deleted file mode 100644 index d3102e4..0000000 --- a/src/controllers/internal/upcoming-titles/anilist.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { env } from "cloudflare:workers"; -import type { HonoRequest } from "hono"; -import { DateTime } from "luxon"; - -import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; -import { getValue, setValue } from "~/models/kv"; -import { filterUnreleasedTitles } from "~/models/unreleasedTitles"; -import type { Title } from "~/types/title"; - -type AiringSchedule = { - media: Title; - episode: number; - timeUntilAiring: number; - airingAt: number; - id: number; -}; - -export async function getUpcomingTitlesFromAnilist(req: HonoRequest) { - const durableObjectId = env.ANILIST_DO.idFromName("GLOBAL"); - const stub = env.ANILIST_DO.get(durableObjectId); - - const lastCheckedScheduleAt = await getValue("schedule_last_checked_at").then( - (value) => (value ? Number(value) : DateTime.now().toUnixInteger()), - ); - const twoDaysFromNow = DateTime.now().plus({ days: 2 }).toUnixInteger(); - - let currentPage = 1; - let plannedToWatchTitles = new Set(); - let scheduleList: AiringSchedule[] = []; - let shouldContinue = true; - - do { - const Page = await stub.getUpcomingTitles( - currentPage++, - lastCheckedScheduleAt, - twoDaysFromNow, - ); - - if (!Page) break; - - const { airingSchedules, pageInfo } = Page; - plannedToWatchTitles = plannedToWatchTitles.union( - await filterUnreleasedTitles( - airingSchedules!.map((schedule: any) => schedule!.media?.id!), - ), - ); - scheduleList = scheduleList.concat( - airingSchedules!.filter( - (schedule: any): schedule is AiringSchedule => - !!schedule && - !plannedToWatchTitles.has(schedule.media?.id) && - schedule.media?.countryOfOrigin === "JP" && - schedule.episode == 1, - ), - ); - shouldContinue = pageInfo?.hasNextPage ?? false; - } while (shouldContinue); - - await Promise.all( - Array.from(plannedToWatchTitles).map((titleId) => - maybeScheduleNextAiringEpisode(titleId), - ), - ); - - if (scheduleList.length === 0) { - return []; - } - - await setValue( - "schedule_last_checked_at", - scheduleList[scheduleList.length - 1].airingAt.toString(), - ); - - return scheduleList; -} diff --git a/src/controllers/internal/upcoming-titles/index.ts b/src/controllers/internal/upcoming-titles/index.ts deleted file mode 100644 index 01a20a9..0000000 --- a/src/controllers/internal/upcoming-titles/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Hono } from "hono"; -import { DateTime } from "luxon"; - -import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials"; -import { sendFcmMessage } from "~/libs/gcloud/sendFcmMessage"; -import { SuccessResponse } from "~/types/schema"; - -import { getUpcomingTitlesFromAnilist } from "./anilist"; - -const app = new Hono(); - -app.post("/", async (c) => { - const titles = await getUpcomingTitlesFromAnilist(c.req); - - await Promise.allSettled( - titles.map(async (title) => { - const titleName = - title.media.title?.userPreferred ?? - title.media.title?.english ?? - "Unknown Title"; - - return sendFcmMessage(getAdminSdkCredentials(), { - topic: "newTitles", - data: { - type: "new_title", - aniListId: title.media.id.toString(), - title: titleName, - airingAt: title.airingAt.toString(), - }, - notification: { - title: "New Series Alert", - body: `${titleName} will be released ${DateTime.fromSeconds(title.airingAt).toRelative({ unit: ["hours", "minutes"] })}`, - image: - title.media.coverImage?.medium ?? - title.media.coverImage?.large ?? - title.media.coverImage?.extraLarge ?? - undefined, - }, - android: { - notification: { - click_action: "HANDLE_FCM_NOTIFICATION", - }, - }, - }); - }), - ); - - return c.json(SuccessResponse, 200); -}); - -export default app; diff --git a/src/controllers/popular/browse/index.ts b/src/controllers/popular/browse/index.ts deleted file mode 100644 index 56f8684..0000000 --- a/src/controllers/popular/browse/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; - -import { ErrorResponse, SuccessResponseSchema } from "~/types/schema"; -import { HomeTitle } from "~/types/title/homeTitle"; - -import { fetchPopularTitlesFromAnilist } from "./anilist"; - -const BrowsePopularResponse = SuccessResponseSchema( - z.object({ - trending: z.array(HomeTitle), - popular: z.array(HomeTitle), - upcoming: z.array(HomeTitle).optional(), - }), -); - -const app = new OpenAPIHono(); - -const route = createRoute({ - tags: ["aniplay", "title"], - operationId: "browsePopularTitles", - summary: "Get a preview of popular titles", - method: "get", - path: "/", - request: { - query: z.object({ - limit: z - .number({ coerce: true }) - .int() - .default(10) - .describe("The number of titles to return"), - }), - }, - responses: { - 200: { - content: { - "application/json": { - schema: BrowsePopularResponse, - }, - }, - description: "Returns an object containing a preview of popular titles", - }, - }, -}); - -app.openapi(route, async (c) => { - const limit = Number(c.req.query("limit") ?? 10); - - const response = await fetchPopularTitlesFromAnilist(limit); - if (!response) { - return c.json(ErrorResponse, { status: 500 }); - } - - return c.json({ success: true, result: response }); -}); - -export default app; diff --git a/src/controllers/popular/category/index.ts b/src/controllers/popular/category/index.ts deleted file mode 100644 index b8b449a..0000000 --- a/src/controllers/popular/category/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; - -import { - ErrorResponse, - PaginatedResponseSchema, - SuccessResponseSchema, -} from "~/types/schema"; -import { HomeTitle } from "~/types/title/homeTitle"; - -import { fetchPopularTitlesFromAnilist } from "./anilist"; -import { PopularCategory } from "./enum"; - -const BrowsePopularResponse = PaginatedResponseSchema(HomeTitle); - -const app = new OpenAPIHono(); - -const route = createRoute({ - tags: ["aniplay", "title"], - operationId: "browsePopularTitlesWithCategory", - summary: "Get a preview of popular titles for a category", - method: "get", - path: "/{category}", - request: { - query: z.object({ - limit: z - .number({ coerce: true }) - .int() - .default(10) - .describe("The number of titles to return"), - page: z.number({ coerce: true }).int().min(1).default(1), - }), - params: z.object({ category: PopularCategory }), - }, - responses: { - 200: { - content: { - "application/json": { - schema: BrowsePopularResponse, - }, - }, - description: "Returns an object containing a preview of popular titles", - }, - }, -}); - -app.openapi(route, async (c) => { - const page = Number(c.req.query("page") ?? 1); - const limit = Number(c.req.query("limit") ?? 10); - const popularCategory = c.req.param("category") as PopularCategory; - - const response = await fetchPopularTitlesFromAnilist( - popularCategory, - page, - limit, - ); - if (!response) { - return c.json(ErrorResponse, { status: 500 }); - } - - return c.json({ - success: true, - results: response.results, - hasNextPage: response.hasNextPage ?? false, - }); -}); - -export default app; diff --git a/src/controllers/popular/index.ts b/src/controllers/popular/index.ts deleted file mode 100644 index e74d001..0000000 --- a/src/controllers/popular/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { OpenAPIHono } from "@hono/zod-openapi"; - -const app = new OpenAPIHono(); - -app.route( - "/browse", - await import("./browse").then((controller) => controller.default), -); - -app.route( - "/", - await import("./category").then((controller) => controller.default), -); - -export default app; diff --git a/src/controllers/search/__snapshots__/index.spec.ts.snap b/src/controllers/search/__snapshots__/index.spec.ts.snap deleted file mode 100644 index ae10e64..0000000 --- a/src/controllers/search/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,4303 +0,0 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": { - "english": "Solo Leveling", - "userPreferred": "Ore dake Level Up na Ken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": { - "english": "Evangelion: 1.0 You Are (Not) Alone", - "userPreferred": "Evangelion Shin Movie: Jo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": { - "english": "Kotaro Lives Alone", - "userPreferred": "Kotarou wa Hitorigurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": { - "english": "I've Somehow Gotten Stronger When I Improved My Farm-Related Skills", - "userPreferred": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": { - "english": "Solo Leveling Season 2 -Arise from the Shadow-", - "userPreferred": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": { - "english": null, - "userPreferred": "sola", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": { - "english": null, - "userPreferred": "Holo no Graffiti", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": { - "english": "Armored Trooper Votoms", - "userPreferred": "Soukou Kihei Votoms", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": { - "english": "Sol Levante", - "userPreferred": "Sol Levante", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": { - "english": null, - "userPreferred": "Sono Toki, Kanojo wa.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": { - "english": "Wooser's Hand-to-Mouth Life", - "userPreferred": "Wooser no Sono Higurashi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": { - "english": null, - "userPreferred": "Kuroshitsuji Picture Drama", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": { - "english": null, - "userPreferred": "sola Specials", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": { - "english": null, - "userPreferred": "SOL BIANCA", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": { - "english": null, - "userPreferred": "Onna no Sono no Hoshi", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": { - "english": "Sol Bianca: The Legacy", - "userPreferred": "Sol Bianca: Taiyou no Fune", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": { - "english": "The Adventures of Scamper the Penguin", - "userPreferred": "Chiisana Pengin: Lolo no Bouken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": { - "english": "Planetarium", - "userPreferred": "Planetarium", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": { - "english": null, - "userPreferred": "Furudera no Obake-soudou", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": { - "english": "P.E.T.", - "userPreferred": "P.E.T.", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": { - "english": "The Garden of Pleasure", - "userPreferred": "Kairaku no Sono", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": { - "english": "Mosh Race", - "userPreferred": "Mosh Race", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": { - "english": "Marco Polo's Adventures", - "userPreferred": "Marco Polo no Boken", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": { - "english": null, - "userPreferred": "SOL", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": { - "english": null, - "userPreferred": "Sono Mukou no Mukougawa", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": { - "english": null, - "userPreferred": "Soko ni wa Mata Meikyuu", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": { - "english": "Rennyo and His Mother", - "userPreferred": "Rennyo to Sono Haha", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 3rd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": { - "english": null, - "userPreferred": "Toute wa Sono Kotae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": { - "english": "Journey to the beyond", - "userPreferred": "Sono Saki no Taniji", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": { - "english": null, - "userPreferred": "Ganbare! Lulu Lolo 2nd Season", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": { - "english": "Short Animations of Junpei Fujita", - "userPreferred": "Tanpen Animation Junpei Fujita", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": { - "english": "A Place to Name", - "userPreferred": "Sono Ie no Namae", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": { - "english": "In Inertia", - "userPreferred": "Guzu no Soko", - }, - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": { - "english": "Indoor Days", - "userPreferred": "Soto ni Denai hi", - }, - }, - ], - "success": true, -} -`; - -exports[`requests the "/search" route valid query that returns anilist results 1`] = ` -{ - "hasNextPage": false, - "results": [ - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx151807-yxY3olrjZH4k.png", - "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", - }, - "id": 151807, - "title": "Ore dake Level Up na Ken", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2759-z07kq8Pnw5B1.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2759-z07kq8Pnw5B1.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2759-z07kq8Pnw5B1.jpg", - }, - "id": 2759, - "title": "Evangelion Shin Movie: Jo", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx139589-oFz7JwpwRkQV.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx139589-oFz7JwpwRkQV.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx139589-oFz7JwpwRkQV.png", - }, - "id": 139589, - "title": "Kotarou wa Hitorigurashi", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx145815-XsgcXy7WzgtK.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145815-XsgcXy7WzgtK.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx145815-XsgcXy7WzgtK.png", - }, - "id": 145815, - "title": "Noumin Kanren no Skill Bakka Agetetara Naze ka Tsuyoku Natta.", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx176496-r6oXxEqdZL0n.jpg", - "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", - }, - "id": 176496, - "title": "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1965-lWBpcTni9PS9.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1965-lWBpcTni9PS9.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1965-lWBpcTni9PS9.png", - }, - "id": 1965, - "title": "sola", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx118123-xqn5fYsjKXJU.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx118123-xqn5fYsjKXJU.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx118123-xqn5fYsjKXJU.png", - }, - "id": 118123, - "title": "Holo no Graffiti", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx2582-aB1Vh1jDobQ3.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx2582-aB1Vh1jDobQ3.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx2582-aB1Vh1jDobQ3.jpg", - }, - "id": 2582, - "title": "Soukou Kihei Votoms", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx116384-xn0nQAKGFSd7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx116384-xn0nQAKGFSd7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx116384-xn0nQAKGFSd7.png", - }, - "id": 116384, - "title": "Sol Levante", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx104073-OQ8YBTy7zmKf.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx104073-OQ8YBTy7zmKf.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx104073-OQ8YBTy7zmKf.jpg", - }, - "id": 104073, - "title": "Sono Toki, Kanojo wa.", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15313.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15313.jpg", - }, - "id": 15313, - "title": "Wooser no Sono Higurashi", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/8068.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/8068.jpg", - }, - "id": 8068, - "title": "Kuroshitsuji Picture Drama", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/3174.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/3174.jpg", - }, - "id": 3174, - "title": "sola Specials", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/1443.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/1443.jpg", - }, - "id": 1443, - "title": "SOL BIANCA", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx153431-DMBYQxagH3Uu.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153431-DMBYQxagH3Uu.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx153431-DMBYQxagH3Uu.jpg", - }, - "id": 153431, - "title": "Onna no Sono no Hoshi", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx1444-7Yn6hmQ2bk9D.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1444-7Yn6hmQ2bk9D.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx1444-7Yn6hmQ2bk9D.png", - }, - "id": 1444, - "title": "Sol Bianca: Taiyou no Fune", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/4138.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/4138.jpg", - }, - "id": 4138, - "title": "Chiisana Pengin: Lolo no Bouken", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx164192-KQ8sYXbaAl6i.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx164192-KQ8sYXbaAl6i.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx164192-KQ8sYXbaAl6i.png", - }, - "id": 164192, - "title": "Planetarium", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b5838-QTe07RRZylUm.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b5838-QTe07RRZylUm.jpg", - }, - "id": 5838, - "title": "Furudera no Obake-soudou", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162882-OQENM5pXn7QQ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162882-OQENM5pXn7QQ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162882-OQENM5pXn7QQ.jpg", - }, - "id": 162882, - "title": "P.E.T.", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/102710-dVayaOkzATwa.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/102710-dVayaOkzATwa.png", - }, - "id": 102710, - "title": "Kairaku no Sono", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx162881-c7xmNA6DlHFZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx162881-c7xmNA6DlHFZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx162881-c7xmNA6DlHFZ.jpg", - }, - "id": 162881, - "title": "Mosh Race", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/5935.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/5935.jpg", - }, - "id": 5935, - "title": "Marco Polo no Boken", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103449-FxDK8eJuMAKg.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103449-FxDK8eJuMAKg.jpg", - }, - "id": 103449, - "title": "SOL", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/12993.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/12993.jpg", - }, - "id": 12993, - "title": "Sono Mukou no Mukougawa", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20459.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20459.jpg", - }, - "id": 20459, - "title": "Ganbare! Lulu Lolo", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b137760-CleNdfmuKRy7.png", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b137760-CleNdfmuKRy7.png", - }, - "id": 137760, - "title": "Soko ni wa Mata Meikyuu", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/7473.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/7473.jpg", - }, - "id": 7473, - "title": "Rennyo to Sono Haha", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/21418-TZYwdItidowx.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/21418-TZYwdItidowx.jpg", - }, - "id": 21418, - "title": "Ganbare! Lulu Lolo 3rd Season", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/103517-XgOUryeFaPDW.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/103517-XgOUryeFaPDW.jpg", - }, - "id": 103517, - "title": "Toute wa Sono Kotae", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b113572-hP9x1SkRJXvA.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b113572-hP9x1SkRJXvA.jpg", - }, - "id": 113572, - "title": "Sono Saki no Taniji", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/20864.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/20864.jpg", - }, - "id": 20864, - "title": "Ganbare! Lulu Lolo 2nd Season", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/15129.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/15129.jpg", - }, - "id": 15129, - "title": "Tanpen Animation Junpei Fujita", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/nx106557-TPLmwa2EccB9.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx106557-TPLmwa2EccB9.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/nx106557-TPLmwa2EccB9.jpg", - }, - "id": 106557, - "title": "Sono Ie no Namae", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b118133-y7RvDFmr30hZ.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/b118133-y7RvDFmr30hZ.jpg", - }, - "id": 118133, - "title": "Guzu no Soko", - }, - { - "coverImage": { - "extraLarge": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx169686-exScHzB5UX2D.jpg", - "large": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx169686-exScHzB5UX2D.jpg", - "medium": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx169686-exScHzB5UX2D.jpg", - }, - "id": 169686, - "title": "Soto ni Denai hi", - }, - ], - "success": true, -} -`; diff --git a/src/controllers/search/index.spec.ts b/src/controllers/search/index.spec.ts deleted file mode 100644 index 78af1ca..0000000 --- a/src/controllers/search/index.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from "bun:test"; - -import app from "~/index"; -import { server } from "~/mocks"; - -server.listen(); - -describe('requests the "/search" route', () => { - it("valid query that returns anilist results", async () => { - const response = await app.request("/search?query=search query"); - - expect(response.json()).resolves.toMatchSnapshot(); - }); - - it("query that returns no results", async () => { - const response = await app.request("/search?query=a"); - - expect(response.json()).resolves.toEqual({ - success: true, - results: [], - hasNextPage: false, - }); - expect(response.status).toBe(200); - }); -}); diff --git a/src/controllers/search/index.ts b/src/controllers/search/index.ts deleted file mode 100644 index 9fb8424..0000000 --- a/src/controllers/search/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; - -import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; -import { PaginatedResponseSchema } from "~/types/schema"; -import { HomeTitle } from "~/types/title/homeTitle"; - -import { fetchSearchResultsFromAnilist } from "./anilist"; - -const app = new OpenAPIHono(); - -const route = createRoute({ - tags: ["aniplay", "title"], - operationId: "search", - summary: "Search for a title", - method: "get", - path: "/", - request: { - query: z.object({ - query: z.string(), - page: z.number({ coerce: true }).int().min(1).default(1), - limit: z.number({ coerce: true }).int().default(10), - }), - }, - responses: { - 200: { - content: { - "application/json": { - schema: PaginatedResponseSchema(HomeTitle), - }, - }, - description: "Returns a list of paginated results for the query", - }, - }, -}); - -app.openapi(route, async (c) => { - const query = c.req.query("query") ?? ""; - const page = Number(c.req.query("page") ?? 1); - const limit = Number(c.req.query("limit") ?? 10); - - // Check if we should use mock data - const { useMockData } = await import("~/libs/useMockData"); - if (useMockData()) { - const { mockSearchResults } = await import("~/mocks/mockData"); - - // Paginate mock results - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - const paginatedResults = mockSearchResults.slice(startIndex, endIndex); - const hasNextPage = endIndex < mockSearchResults.length; - - return c.json( - { - success: true, - results: paginatedResults, - hasNextPage, - }, - 200, - ); - } - - const { result: response, errorOccurred } = await fetchFromMultipleSources([ - () => fetchSearchResultsFromAnilist(query, page, limit), - ]); - - if (!response) { - return c.json({ - success: !errorOccurred, - results: [], - hasNextPage: false, - }); - } - - return c.json( - { - success: true, - results: response.results, - hasNextPage: response.hasNextPage ?? false, - }, - 200, - ); -}); - -export default app; diff --git a/src/controllers/title/__snapshots__/index.spec.ts.snap b/src/controllers/title/__snapshots__/index.spec.ts.snap deleted file mode 100644 index fed38ab..0000000 --- a/src/controllers/title/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,703 +0,0 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id & token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": { - "id": 402665918, - "progress": 1, - "status": "CURRENT", - }, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; - -exports[`requests the "/title" route with a valid id but no token 1`] = ` -{ - "result": { - "averageScore": 66, - "bannerImage": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", - "countryOfOrigin": "JP", - "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", - }, - "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?" -

-The 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. -

-(Source: Netflix Anime)" -, - "episodes": 6, - "genres": [ - "Fantasy", - "Thriller", - ], - "id": 135643, - "idMal": 49210, - "mediaListEntry": null, - "nextAiringEpisode": null, - "status": "FINISHED", - "title": { - "english": "The Grimm Variations", - "userPreferred": "The Grimm Variations", - }, - }, - "success": true, -} -`; diff --git a/src/controllers/title/index.spec.ts b/src/controllers/title/index.spec.ts deleted file mode 100644 index cb72790..0000000 --- a/src/controllers/title/index.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -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.toMatchSnapshot(); - 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.toMatchSnapshot(); - 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); - }); -}); diff --git a/src/controllers/title/index.ts b/src/controllers/title/index.ts deleted file mode 100644 index cbc1e27..0000000 --- a/src/controllers/title/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; - -import { fetchTitleFromAnilist } from "~/libs/anilist/getTitle"; -import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; -import { - AniListIdQuerySchema, - ErrorResponse, - ErrorResponseSchema, - SuccessResponseSchema, -} from "~/types/schema"; -import { Title } from "~/types/title"; - -const app = new OpenAPIHono(); - -const route = createRoute({ - tags: ["aniplay", "title"], - operationId: "fetchTitle", - summary: "Fetch title information", - method: "get", - path: "/", - request: { - query: z.object({ id: AniListIdQuerySchema }), - 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"); - - // Check if we should use mock data - const { useMockData } = await import("~/libs/useMockData"); - if (useMockData()) { - const { mockTitleDetails } = await import("~/mocks/mockData"); - - return c.json({ success: true, result: mockTitleDetails() }, 200); - } - - const { result: title, errorOccurred } = await fetchFromMultipleSources([ - () => fetchTitleFromAnilist(aniListId, aniListToken ?? undefined), - ]); - - if (errorOccurred) { - console.error(`Failed to fetch title ${aniListId}`); - return c.json(ErrorResponse, { status: 500 }); - } - - if (!title) { - return c.json(ErrorResponse, 404); - } - - return c.json({ success: true, result: title }, 200); -}); - -export default app; diff --git a/src/controllers/token/index.spec.ts b/src/controllers/token/index.spec.ts deleted file mode 100644 index 7450058..0000000 --- a/src/controllers/token/index.spec.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { eq } from "drizzle-orm"; -import { DateTime } from "luxon"; - -import { beforeEach, describe, expect, it, mock } from "bun:test"; - -import app from "~/index"; -import { getTestDb } from "~/libs/test/getTestDb"; -import { resetTestDb } from "~/libs/test/resetTestDb"; -import { server } from "~/mocks"; -import { deviceTokensTable } from "~/models/schema"; - -server.listen(); - -describe("requests the /token route", () => { - const db = getTestDb(); - - beforeEach(async () => { - await resetTestDb(); - mock.module("src/libs/gcloud/verifyFcmToken", () => ({ - verifyFcmToken: () => true, - })); - }); - - it("should succeed", async () => { - const res = await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "123" }), - }); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("succeeded, db should contain entry", async () => { - const minimumTimestamp = DateTime.now(); - await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "123" }), - }); - - const row = await db - .select() - .from(deviceTokensTable) - .where(eq(deviceTokensTable.deviceId, "123")) - .get(); - - expect(row).toEqual({ - deviceId: "123", - token: "123", - username: null, - lastConnectedAt: expect.any(String), - }); - // since SQL timestamp doesn't support milliseconds, compare to nearest second - expect( - +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( - "second", - ), - ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); - }); - - it("device id already exists in db, should succeed", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "123" }); - - const res = await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "124", deviceId: "123" }), - }); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("device id already exists in db, should contain new token", async () => { - const minimumTimestamp = DateTime.now(); - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "123" }); - await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "124", deviceId: "123" }), - }); - - const row = await db - .select() - .from(deviceTokensTable) - .where(eq(deviceTokensTable.deviceId, "123")) - .get(); - - expect(row).toEqual({ - deviceId: "123", - token: "124", - username: null, - lastConnectedAt: expect.any(String), - }); - // since SQL timestamp doesn't support milliseconds, compare to nearest second - expect( - +DateTime.fromSQL(row!.lastConnectedAt!, { zone: "utc" }).startOf( - "second", - ), - ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); - }); - - it("token already exists in db, should not insert new entry", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "123" }); - await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "124" }), - }); - - const row = await db - .select() - .from(deviceTokensTable) - .where(eq(deviceTokensTable.deviceId, "124")) - .get(); - - expect(row).toBeUndefined(); - }); - - it("token is invalid, should fail", async () => { - mock.module("src/libs/gcloud/verifyFcmToken", () => ({ - verifyFcmToken: () => false, - })); - - const res = await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "124" }), - }); - - expect(res.json()).resolves.toEqual({ success: false }); - expect(res.status).toBe(401); - }); - - it("token is invalid, should not insert new entry", async () => { - mock.module("src/libs/gcloud/verifyFcmToken", () => ({ - verifyFcmToken: () => false, - })); - await app.request("/token", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify({ token: "123", deviceId: "124" }), - }); - - const row = await db - .select() - .from(deviceTokensTable) - .where(eq(deviceTokensTable.deviceId, "124")) - .get(); - - expect(row).toBeUndefined(); - }); -}); diff --git a/src/controllers/token/index.ts b/src/controllers/token/index.ts deleted file mode 100644 index c17db59..0000000 --- a/src/controllers/token/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import { env } from "hono/adapter"; - -import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials"; -import { verifyFcmToken } from "~/libs/gcloud/verifyFcmToken"; -import { saveToken } from "~/models/token"; -import { - ErrorResponse, - ErrorResponseSchema, - SuccessResponse, - SuccessResponseSchema, -} from "~/types/schema"; - -const app = new OpenAPIHono(); - -const SaveTokenRequest = z.object({ - token: z.string(), - deviceId: z.string(), -}); - -const SaveTokenResponse = SuccessResponseSchema(); - -const route = createRoute({ - tags: ["aniplay", "notifications"], - operationId: "saveToken", - summary: "Saves FCM token", - method: "post", - path: "/", - request: { - body: { - content: { - "application/json": { - schema: SaveTokenRequest, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: SaveTokenResponse, - }, - }, - description: "Saved token successfully", - }, - 412: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Token already exists", - }, - 500: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Unknown error occurred", - }, - }, -}); - -app.openapi(route, async (c) => { - const { token, deviceId } = await c.req.json(); - - try { - const isValidToken = await verifyFcmToken(token, getAdminSdkCredentials()); - if (!isValidToken) { - return c.json(ErrorResponse, 401); - } - - await saveToken(deviceId, token); - } catch (error) { - console.error("Failed to save token"); - console.error(error); - return c.json(ErrorResponse, 500); - } - - return c.json(SuccessResponse); -}); - -export default app; diff --git a/src/controllers/watch-status/index.spec.ts b/src/controllers/watch-status/index.spec.ts deleted file mode 100644 index 6491c17..0000000 --- a/src/controllers/watch-status/index.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { eq } from "drizzle-orm"; - -import { beforeEach, describe, expect, it } from "bun:test"; - -import app from "~/index"; -import { getTestDb } from "~/libs/test/getTestDb"; -import { getTestEnv } from "~/libs/test/getTestEnv"; -import { resetTestDb } from "~/libs/test/resetTestDb"; -import { server } from "~/mocks"; -import { deviceTokensTable, watchStatusTable } from "~/models/schema"; - -server.listen(); - -describe("requests the /watch-status route", () => { - const db = getTestDb(); - - beforeEach(async () => { - await resetTestDb(); - }); - - it("saving title, deviceId in db, should succeed", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "asd" }); - - const res = await app.request( - "/watch-status", - { - method: "POST", - headers: new Headers({ - "x-anilist-token": "asd", - "Content-Type": "application/json", - }), - body: JSON.stringify({ - deviceId: "123", - watchStatus: "CURRENT", - titleId: 10, - }), - }, - getTestEnv(), - ); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("saving title, deviceId not in db, should fail", async () => { - const res = await app.request( - "/watch-status", - { - method: "POST", - headers: new Headers({ - "x-anilist-token": "asd", - "Content-Type": "application/json", - }), - body: JSON.stringify({ - deviceId: "123", - watchStatus: "CURRENT", - titleId: 10, - }), - }, - getTestEnv(), - ); - - expect(res.json()).resolves.toEqual({ success: false }); - expect(res.status).toBe(500); - }); - - it("saving title, Anilist request fails, should succeed", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "asd" }); - - const res = await app.request( - "/watch-status", - { - method: "POST", - headers: new Headers({ - "x-anilist-token": "asd", - "Content-Type": "application/json", - }), - body: JSON.stringify({ - deviceId: "123", - watchStatus: "CURRENT", - titleId: -1, - }), - }, - getTestEnv(), - ); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("watch status is null, should succeed", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "asd" }); - - const res = await app.request( - "/watch-status", - { - method: "POST", - headers: new Headers({ - "x-anilist-token": "asd", - "Content-Type": "application/json", - }), - body: JSON.stringify({ - deviceId: "123", - watchStatus: null, - titleId: 10, - }), - }, - getTestEnv(), - ); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("watch status is null, title does not exist, should succeed", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "asd" }); - - const res = await app.request( - "/watch-status", - { - method: "POST", - headers: new Headers({ - "x-anilist-token": "asd", - "Content-Type": "application/json", - }), - body: JSON.stringify({ - deviceId: "123", - watchStatus: null, - titleId: -1, - }), - }, - getTestEnv(), - ); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("watch status is null, title exists, fails to delete entry, should succeed", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "asd" }); - - const res = await app.request( - "/watch-status", - { - method: "POST", - headers: new Headers({ - "x-anilist-token": "asd", - "Content-Type": "application/json", - }), - body: JSON.stringify({ - deviceId: "123", - watchStatus: null, - titleId: 139518, - }), - }, - getTestEnv(), - ); - - expect(res.json()).resolves.toEqual({ success: true }); - expect(res.status).toBe(200); - }); - - it("watch status is null, should delete entry", async () => { - await db - .insert(deviceTokensTable) - .values({ deviceId: "123", token: "asd" }); - await db.insert(watchStatusTable).values({ deviceId: "123", titleId: 10 }); - - await app.request( - "/watch-status", - { - method: "POST", - headers: new Headers({ - "x-anilist-token": "asd", - "Content-Type": "application/json", - }), - body: JSON.stringify({ - deviceId: "123", - watchStatus: null, - titleId: 10, - }), - }, - getTestEnv(), - ); - const row = await db - .select() - .from(watchStatusTable) - .where(eq(watchStatusTable.titleId, 10)) - .get(); - - expect(row).toBeUndefined(); - }); -}); diff --git a/src/controllers/watch-status/index.ts b/src/controllers/watch-status/index.ts deleted file mode 100644 index 27b9fd9..0000000 --- a/src/controllers/watch-status/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import type { HonoRequest } from "hono"; - -import { AnilistUpdateType } from "~/libs/anilist/updateType.ts"; -import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; -import { buildNewEpisodeTaskId } from "~/libs/tasks/id"; -import { queueTask } from "~/libs/tasks/queueTask"; -import { removeTask } from "~/libs/tasks/removeTask"; -import { setWatchStatus } from "~/models/watchStatus"; -import { - AniListIdSchema, - ErrorResponse, - ErrorResponseSchema, - SuccessResponse, - SuccessResponseSchema, -} from "~/types/schema"; -import { WatchStatus } from "~/types/title/watchStatus"; - -import { maybeUpdateWatchStatusOnAnilist } from "./anilist"; - -const app = new OpenAPIHono(); - -const UpdateWatchStatusRequest = z.object({ - deviceId: z.string(), - watchStatus: WatchStatus.nullable(), - titleId: AniListIdSchema, - isRetrying: z.boolean().optional().default(false), -}); - -const route = createRoute({ - tags: ["aniplay", "title"], - operationId: "updateWatchStatus", - summary: "Update watch status for a title", - description: - "Updates the watch status for a title. If the user sets the watch status to 'watching', they'll start getting notified about new episodes.", - method: "post", - path: "/", - request: { - body: { - content: { - "application/json": { - schema: UpdateWatchStatusRequest, - }, - }, - }, - headers: z.object({ "x-anilist-token": z.string().nullish() }), - }, - responses: { - 200: { - content: { - "application/json": { - schema: SuccessResponseSchema(), - }, - }, - description: "Watch status was successfully updated", - }, - 500: { - content: { - "application/json": { - schema: ErrorResponseSchema, - }, - }, - description: "Failed to update watch status", - }, - }, -}); - -export async function updateWatchStatus( - deviceId: string, - titleId: number, - watchStatus: WatchStatus | null, -) { - const { wasAdded, wasDeleted } = await setWatchStatus( - deviceId, - Number(titleId), - watchStatus, - ); - if (wasAdded) { - await maybeScheduleNextAiringEpisode(titleId); - } else if (wasDeleted) { - await removeTask("NEW_EPISODE", buildNewEpisodeTaskId(titleId)); - } -} - -app.openapi(route, async (c) => { - const { - deviceId, - watchStatus, - titleId, - isRetrying = false, - } = await c.req.json(); - const aniListToken = c.req.header("X-AniList-Token"); - - // Check if we should use mock data - const { useMockData } = await import("~/libs/useMockData"); - if (useMockData()) { - // Return success immediately without side effects - return c.json(SuccessResponse, { status: 200 }); - } - - if (!isRetrying) { - try { - await updateWatchStatus(c.req, deviceId, titleId, watchStatus); - } catch (error) { - console.error("Error setting watch status"); - console.error(error); - return c.json(ErrorResponse, { status: 500 }); - } - } - - try { - await maybeUpdateWatchStatusOnAnilist( - Number(titleId), - watchStatus, - aniListToken, - ); - } catch (error) { - console.error("Failed to update watch status on Anilist"); - console.error(error); - if (isRetrying) { - return c.json(ErrorResponse, { status: 500 }); - } - - await queueTask( - "ANILIST_UPDATES", - { - deviceId, - watchStatus, - titleId, - updateType: AnilistUpdateType.UpdateWatchStatus, - }, - { req: c.req, scheduleConfig: { delay: { minute: 1 } } }, - ); - } - - return c.json(SuccessResponse, { status: 200 }); -}); - -export default app; diff --git a/src/graphql.ts b/src/graphql.ts new file mode 100644 index 0000000..83090dd --- /dev/null +++ b/src/graphql.ts @@ -0,0 +1,41 @@ +import { createSchema, createYoga } from "graphql-yoga"; +import { Hono } from "hono"; + +import { createGraphQLContext } from "./context"; +import { resolvers } from "./resolvers"; +import { typeDefs } from "./schema"; + +const schema = createSchema({ + typeDefs, + resolvers, +}); + +const yoga = createYoga({ + schema, + graphqlEndpoint: "/graphql", + landingPage: false, // Disable landing page for production + graphiql: { + title: "Aniplay GraphQL API", + }, + context: ({ request }) => { + // Extract Hono context from the request + // graphql-yoga passes the raw request, but we need Hono context + // This will be provided when we integrate with Hono + return request as any; + }, +}); + +const app = new Hono(); + +app.all("/", async (c) => { + const graphqlContext = await createGraphQLContext(c); + + // Create a custom request object that includes our GraphQL context + const request = c.req.raw.clone(); + (request as any).graphqlContext = graphqlContext; + + const response = await yoga.fetch(request, graphqlContext); + return response; +}); + +export default app; diff --git a/src/graphql/context.ts b/src/graphql/context.ts deleted file mode 100644 index 5204e11..0000000 --- a/src/graphql/context.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Context as HonoContext } from "hono"; - -export interface GraphQLContext { - db: D1Database; - deviceId?: string; - aniListToken?: string; - user: { id: number, name: string } | null; - honoContext: HonoContext; -} - -export async function createGraphQLContext(c: HonoContext): Promise { - const deviceId = c.req.header("X-Device-ID"); - const aniListToken = c.req.header("X-AniList-Token"); - const env = c.env as Env; - - const stub = await env.ANILIST_DO.getByName("GLOBAL"); - const user = await stub.getUser(aniListToken!); - - return { - db: env.DB, - deviceId, - aniListToken, - user, - honoContext: c, - }; -} diff --git a/src/graphql/index.ts b/src/graphql/index.ts deleted file mode 100644 index dd52720..0000000 --- a/src/graphql/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { createSchema, createYoga } from "graphql-yoga"; -import { Hono } from "hono"; - -import { createGraphQLContext } from "./context"; -import { resolvers } from "./resolvers"; -import { typeDefs } from "./schema"; - -const schema = createSchema({ - typeDefs, - resolvers, -}); - -const yoga = createYoga({ - schema, - graphqlEndpoint: "/graphql", - landingPage: false, // Disable landing page for production - graphiql: { - title: "Aniplay GraphQL API", - }, - context: ({ request }) => { - // Extract Hono context from the request - // graphql-yoga passes the raw request, but we need Hono context - // This will be provided when we integrate with Hono - return request as any; - }, -}); - -const app = new Hono(); - -app.all("/", async (c) => { - const graphqlContext = await createGraphQLContext(c); - - // Create a custom request object that includes our GraphQL context - const request = c.req.raw.clone(); - (request as any).graphqlContext = graphqlContext; - - const response = await yoga.fetch(request, graphqlContext); - return response; -}); - -export default app; diff --git a/src/graphql/resolvers/queries/home.ts b/src/graphql/resolvers/queries/home.ts deleted file mode 100644 index 40f64e3..0000000 --- a/src/graphql/resolvers/queries/home.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { GraphQLContext } from "~/graphql/context"; - -import { GraphQLError } from "graphql"; -import { graphql } from "gql.tada"; -import { MediaFragment } from "~/types/title/mediaFragment"; -import { env } from "cloudflare:workers"; - -enum HomeCategory { - WATCHING, - PLANNING, -} - -export async function home(_parent: any, args: { category: HomeCategory, page?: number }, context: GraphQLContext) { - const { category, page = 1 } = args; - const { user, aniListToken } = context; - let statusFilters: string[] = []; - switch (category) { - case HomeCategory.WATCHING: - statusFilters = ['CURRENT']; - break; - case HomeCategory.PLANNING: - statusFilters = ['PLANNING', 'PAUSED', 'REPEATING']; - break; - } - - const stub = await env.ANILIST_DO.getByName("GLOBAL"); - const response = await stub.getTitles(user?.name, page, statusFilters, aniListToken); - - if (!response) { - throw new GraphQLError(`Failed to fetch ${category} titles`, { - extensions: { code: "INTERNAL_SERVER_ERROR" }, - }); - } - - return response; -} \ No newline at end of file diff --git a/src/graphql/resolvers/queries/user.ts b/src/graphql/resolvers/queries/user.ts deleted file mode 100644 index 5f498b7..0000000 --- a/src/graphql/resolvers/queries/user.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getUser } from "~/controllers/auth/anilist/getUser"; -import type { GraphQLContext } from "~/graphql/context"; -import { GraphQLError } from "graphql"; - -export async function user(_parent: any, _args: {}, context: GraphQLContext) { - const { aniListToken } = context; - if (!aniListToken) { - throw new GraphQLError("Unauthorized", { - extensions: { code: "UNAUTHORIZED" }, - }); - } - - const response = await getUser(aniListToken); - if (!response) { - throw new GraphQLError(`Failed to fetch user`, { - extensions: { code: "INTERNAL_SERVER_ERROR" }, - }); - } - - return response; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 18990ce..17ee82f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import { Hono } from "hono"; -import { maybeUpdateLastConnectedAt } from "~/controllers/maybeUpdateLastConnectedAt"; +import { onNewEpisode } from "~/jobs/new-episode"; import type { QueueName } from "~/libs/tasks/queueName.ts"; +import { maybeUpdateLastConnectedAt } from "~/middleware/maybeUpdateLastConnectedAt"; -import { onNewEpisode } from "./controllers/internal/new-episode"; import type { QueueBody } from "./libs/tasks/queueTask"; const app = new Hono(); diff --git a/src/controllers/internal/new-episode/index.ts b/src/jobs/new-episode.ts similarity index 64% rename from src/controllers/internal/new-episode/index.ts rename to src/jobs/new-episode.ts index 1b62246..9e4390a 100644 --- a/src/controllers/internal/new-episode/index.ts +++ b/src/jobs/new-episode.ts @@ -1,21 +1,11 @@ -import { zValidator } from "@hono/zod-validator"; -import { Hono } from "hono"; -import { z } from "zod"; - -import { getEpisodesFromAniwatch } from "~/controllers/episodes/getByAniListId/aniwatch"; -import { fetchEpisodeUrl } from "~/controllers/episodes/getEpisodeUrl"; import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials"; import { sendFcmMessage } from "~/libs/gcloud/sendFcmMessage"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; import { getTokensSubscribedToTitle } from "~/models/token"; import { isWatchingTitle } from "~/models/watchStatus"; -import { - AniListIdSchema, - EpisodeNumberSchema, - SuccessResponse, -} from "~/types/schema"; - -const app = new Hono(); +import { getEpisodesFromAniwatch } from "~/services/episodes/getByAniListId/aniwatch"; +import { fetchEpisodeUrl } from "~/services/episodes/getEpisodeUrl"; +import { SuccessResponse } from "~/types/schema"; export async function onNewEpisode(aniListId: number, episodeNumber: number) { console.log( @@ -56,29 +46,3 @@ export async function onNewEpisode(aniListId: number, episodeNumber: number) { return SuccessResponse; } - -app.post( - "/", - zValidator( - "json", - z.object({ - aniListId: AniListIdSchema, - episodeNumber: EpisodeNumberSchema, - }), - ), - async (c) => { - const { aniListId, episodeNumber } = await c.req.json<{ - aniListId: number; - episodeNumber: number; - }>(); - - const result = await onNewEpisode(aniListId, episodeNumber, c.req); - if (result.success) { - return c.json(result, 200); - } else { - return c.json(result, 500); - } - }, -); - -export default app; diff --git a/src/libs/anilist/anilist-do.ts b/src/libs/anilist/anilist-do.ts index 1da1dd3..5516800 100644 --- a/src/libs/anilist/anilist-do.ts +++ b/src/libs/anilist/anilist-do.ts @@ -1,6 +1,6 @@ import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { DurableObject } from "cloudflare:workers"; -import { type ResultOf } from "gql.tada"; +import { $tada, type ResultOf } from "gql.tada"; import { print } from "graphql"; import { z } from "zod"; diff --git a/src/controllers/maybeUpdateLastConnectedAt.ts b/src/middleware/maybeUpdateLastConnectedAt.ts similarity index 100% rename from src/controllers/maybeUpdateLastConnectedAt.ts rename to src/middleware/maybeUpdateLastConnectedAt.ts diff --git a/src/mocks/anify/episodes.ts b/src/mocks/anify/episodes.ts deleted file mode 100644 index 6bfec47..0000000 --- a/src/mocks/anify/episodes.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { HttpResponse, http } from "msw"; - -export function getAnifyEpisodes() { - return http.get( - "https://anify.eltik.cc/episodes/:aniListId", - ({ params }) => { - const aniListId = Number(params["aniListId"]); - if (aniListId === 3 || aniListId === 4 || aniListId < 0) { - return HttpResponse.json([]); - } - - return HttpResponse.json([ - { - providerId: "zoro", - episodes: [ - { - id: "/watch/spy-classroom-season-2-18468?ep=103233", - isFiller: false, - number: 1, - title: "Mission: Forgetter I", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=103632", - isFiller: false, - number: 2, - title: "Mission: Forgetter II", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=104244", - isFiller: false, - number: 3, - title: "Mission: Forgetter III", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=104620", - isFiller: false, - number: 4, - title: "Mission: Forgetter IV", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=104844", - isFiller: false, - number: 5, - title: "File: Glint", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=105761", - isFiller: false, - number: 6, - title: "File: Dreamspeaker Thea", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=106135", - isFiller: false, - number: 7, - title: "File: Forgetter Annette", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=106518", - isFiller: false, - number: 8, - title: "Mission: Dreamspeaker I", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=106606", - isFiller: false, - number: 9, - title: "Mission: Dreamspeaker II", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=106981", - isFiller: false, - number: 10, - title: "Mission: Dreamspeaker III", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=107176", - isFiller: false, - number: 11, - title: "Mission: Dreamspeaker IV", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - { - id: "/watch/spy-classroom-season-2-18468?ep=107247", - isFiller: false, - number: 12, - title: "File: Flower Garden Lily", - img: null, - hasDub: false, - description: null, - rating: null, - updatedAt: 0, - }, - ], - }, - ]); - }, - ); -} diff --git a/src/mocks/anify/sources.ts b/src/mocks/anify/sources.ts deleted file mode 100644 index 2d741a6..0000000 --- a/src/mocks/anify/sources.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { HttpResponse, http } from "msw"; - -export function getAnifySources() { - return http.post("https://anify.eltik.cc/sources", async ({ request }) => { - const { id: aniListId } = await request.json(); - if (aniListId < 0) { - return HttpResponse.json({ sources: [] }); - } - - return HttpResponse.json({ - sources: [ - { - url: "https://proxy.anify.tv/video/jCB57RSXMJNw%252Bl%252F7FyBhTJgxyu4fxWq%252BaNKwhio1LIFFWpAYK7%252F8XSh%252BAuGkDcb9ncmrm8yVcsjzS1idTV1sEjbb0BtANg2FkrmhfZi4%252Bgg%252F1JfCmyBOq9QkhiZYHedLzHQ8Q6aQc2riLeYsblZY7Kgw%252Filz%252BitXh1tUI97Qd1k%253D/%7B%7D/.m3u8", - quality: "360p", - }, - { - url: "https://proxy.anify.tv/video/Yo7Z6i%252FaG8OYgX8PODTiATrhzRg640USqkzuH1RalwnianjLBAQnbcW3XxVqci8EZw3f6Ui%252FbBC2BpJUOpqLmHOr8GEK%252BRCAvdbXfQ8m5iip%252FWzmMrYp5tcOE6kcFcrPwm1DGNMhz%252BqX3k1Je8QbiuFofSBsCTfmh83vy4uUBhc%253D/%7B%7D/.m3u8", - quality: "480p", - }, - { - url: "https://proxy.anify.tv/video/cqJw05VAzYMnw721FBjS2LG4BTFvwPYYQz9BxZmCy0ZbDMyD4tJGg%252BmsZonVvfDEb%252BL65I8Y9YNCMKB%252BRYkIvpTy9n1dNGp3sTWXk6%252F3nAlhbR8h8iPjbHqaurUhmw5CCV4Po%252BPQuRFubkWdQG2h0n7GqQrv6tn6FfbcoasDiSM%253D/%7B%7D/.m3u8", - quality: "720p", - }, - { - url: "https://proxy.anify.tv/video/MZQCOq%252Baw9w6ywreT8qXviX%252B%252B%252Bhisr%252Bp8qWdyEaCphHla9y%252F4afGVnnObG50pzlK8Km7og6l6v68EKKunByKexiLTivV7oOYMklcZL2Dq3wPleeicg93olUBmztLEvwWWLP8nemmEjy%252BcUBhxaSreVJYzOJpH84hSC7glHsOXig%253D/%7B%7D/.m3u8", - quality: "1080p", - }, - { - url: "https://proxy.anify.tv/video/8CLGIJg8G3k%252BH%252BYV9xyOYVGZ8al8uZqqtbXk44wKRco%252BGATkCrqlkgdRiam3owmOU4f2MAB89GOblOuZbxifwbGsjvp32uxhRC4kZVYrWnZmP%252FrLxtqwi0n6zY%252BvrffUh6dbg6DADSLCWhd2bNUUIg%253D%253D/%7B%7D/.m3u8", - quality: "default", - }, - ], - subtitles: [], - audio: [], - intro: { - start: 0, - end: 0, - }, - outro: { - start: 0, - end: 0, - }, - headers: {}, - }); - }); -} diff --git a/src/mocks/anify/title.ts b/src/mocks/anify/title.ts deleted file mode 100644 index 3532d77..0000000 --- a/src/mocks/anify/title.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { HttpResponse, http } from "msw"; - -export function getAnifyTitle() { - return http.get(`https://anify.eltik.cc/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" }); - }); -} diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 18c0de3..ce385e0 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,6 +1,3 @@ -import { getAnifyEpisodes } from "./anify/episodes"; -import { getAnifySources } from "./anify/sources"; -import { getAnifyTitle } from "./anify/title"; import { deleteAnilistMediaListEntry } from "./anilist/deleteMediaListEntry"; import { getAnilistMediaListEntry } from "./anilist/mediaListEntry"; import { getAnilistNextAiringEpisode } from "./anilist/nextAiringEpisode"; @@ -23,9 +20,6 @@ export const handlers = [ getAnilistSearchResults(), getAnilistTitle(), updateAnilistWatchStatus(), - getAnifyEpisodes(), - getAnifySources(), - getAnifyTitle(), getAniwatchEpisodes(), getAniwatchSearchResults(), getAniwatchSources(), diff --git a/src/mocks/mockData.ts b/src/mocks/mockData.ts index de56935..b7e04ba 100644 --- a/src/mocks/mockData.ts +++ b/src/mocks/mockData.ts @@ -1,3 +1,4 @@ +import type { Episode } from "~/types/episode"; import type { FetchUrlResponseSchema } from "~/types/episode/fetch-url-response"; import type { Title } from "~/types/title"; import type { HomeTitle } from "~/types/title/homeTitle"; @@ -92,12 +93,13 @@ export const mockEpisodeUrl: FetchUrlResponseSchema = { * Mock data for episodes list * Returns a sample list of 50 episodes for testing */ -export const mockEpisodes = () => { +export const mockEpisodes: () => Episode[] = () => { const randomId = Math.floor(Math.random() * 1000000); return Array.from({ length: 50 }, (_, i) => ({ id: `${randomId}-episode-${i + 1}`, number: i + 1, title: `Episode ${i + 1}`, isFiller: false, + updatedAt: 0, })); }; diff --git a/src/models/schema.ts b/src/models/schema.ts index 101fca4..a8b20c7 100644 --- a/src/models/schema.ts +++ b/src/models/schema.ts @@ -31,7 +31,7 @@ export const watchStatusTable = sqliteTable( export const keyValueTable = sqliteTable("key_value", { key: text("key", { - enum: ["schedule_last_checked_at", "anify_killswitch_till"], + enum: ["schedule_last_checked_at"], }).primaryKey(), value: text("value").notNull(), }); diff --git a/src/graphql/resolvers/image.ts b/src/resolvers/image.ts similarity index 100% rename from src/graphql/resolvers/image.ts rename to src/resolvers/image.ts diff --git a/src/graphql/resolvers/index.ts b/src/resolvers/index.ts similarity index 64% rename from src/graphql/resolvers/index.ts rename to src/resolvers/index.ts index 06501f9..ec4e21f 100644 --- a/src/graphql/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -11,19 +11,19 @@ import { user } from "./queries/user"; import { Title } from "./title"; export const resolvers = { - Query: { - healthCheck, - title, - search, - popularBrowse, - popularByCategory, - episodeStream, - user, - }, - Mutation: { - updateWatchStatus: updateWatchStatusMutation, - markEpisodeAsWatched: markEpisodeAsWatchedMutation, - updateToken: updateTokenMutation, - }, - Title, + Query: { + healthCheck, + title, + search, + popularBrowse, + popularByCategory, + episodeStream, + user, + }, + Mutation: { + updateWatchStatus: updateWatchStatusMutation, + markEpisodeAsWatched: markEpisodeAsWatchedMutation, + updateToken: updateTokenMutation, + }, + Title, }; diff --git a/src/graphql/resolvers/mutations/markEpisodeAsWatched.ts b/src/resolvers/mutations/markEpisodeAsWatched.ts similarity index 70% rename from src/graphql/resolvers/mutations/markEpisodeAsWatched.ts rename to src/resolvers/mutations/markEpisodeAsWatched.ts index a566dc7..52d3b48 100644 --- a/src/graphql/resolvers/mutations/markEpisodeAsWatched.ts +++ b/src/resolvers/mutations/markEpisodeAsWatched.ts @@ -1,8 +1,7 @@ import { GraphQLError } from "graphql"; -import { markEpisodeAsWatched } from "~/controllers/episodes/markEpisodeAsWatched/anilist"; - -import type { GraphQLContext } from "../../context"; +import type { GraphQLContext } from "~/context"; +import { markEpisodeAsWatched } from "~/services/episodes/markEpisodeAsWatched/anilist"; interface MarkEpisodeAsWatchedInput { titleId: number; @@ -39,6 +38,17 @@ export async function markEpisodeAsWatchedMutation( input.isComplete, ); + if (input.isComplete) { + if (context.deviceId) { + const { updateWatchStatus } = await import("~/services/watch-status"); + await updateWatchStatus(context.deviceId, input.titleId, "COMPLETED"); + } else { + console.warn( + "Device ID not found in context, skipping watch status update", + ); + } + } + if (!user) { throw new GraphQLError("Failed to mark episode as watched", { extensions: { code: "INTERNAL_SERVER_ERROR" }, diff --git a/src/graphql/resolvers/mutations/updateToken.ts b/src/resolvers/mutations/updateToken.ts similarity index 92% rename from src/graphql/resolvers/mutations/updateToken.ts rename to src/resolvers/mutations/updateToken.ts index ee704aa..96a050f 100644 --- a/src/graphql/resolvers/mutations/updateToken.ts +++ b/src/resolvers/mutations/updateToken.ts @@ -1,9 +1,8 @@ +import type { GraphQLContext } from "~/context"; import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials"; import { verifyFcmToken } from "~/libs/gcloud/verifyFcmToken"; import { saveToken } from "~/models/token"; -import type { GraphQLContext } from "../../context"; - export async function updateTokenMutation( _parent: unknown, args: { token: string }, diff --git a/src/graphql/resolvers/mutations/updateWatchStatus.ts b/src/resolvers/mutations/updateWatchStatus.ts similarity index 88% rename from src/graphql/resolvers/mutations/updateWatchStatus.ts rename to src/resolvers/mutations/updateWatchStatus.ts index 5a67a3e..023ab6a 100644 --- a/src/graphql/resolvers/mutations/updateWatchStatus.ts +++ b/src/resolvers/mutations/updateWatchStatus.ts @@ -1,10 +1,9 @@ import { GraphQLError } from "graphql"; -import { updateWatchStatus } from "~/controllers/watch-status"; +import type { GraphQLContext } from "~/context"; +import { updateWatchStatus } from "~/services/watch-status"; import type { WatchStatus } from "~/types/title/watchStatus"; -import type { GraphQLContext } from "../../context"; - interface UpdateWatchStatusInput { titleId: number; watchStatus: WatchStatus | null; diff --git a/src/graphql/resolvers/queries/episodeStream.ts b/src/resolvers/queries/episodeStream.ts similarity index 73% rename from src/graphql/resolvers/queries/episodeStream.ts rename to src/resolvers/queries/episodeStream.ts index d271775..74a8db8 100644 --- a/src/graphql/resolvers/queries/episodeStream.ts +++ b/src/resolvers/queries/episodeStream.ts @@ -1,6 +1,5 @@ -import { fetchEpisodeUrl } from "~/controllers/episodes/getEpisodeUrl"; - -import type { GraphQLContext } from "../../context"; +import type { GraphQLContext } from "~/context"; +import { fetchEpisodeUrl } from "~/services/episodes/getEpisodeUrl"; export async function episodeStream( _parent: unknown, diff --git a/src/graphql/resolvers/queries/healthCheck.ts b/src/resolvers/queries/healthCheck.ts similarity index 70% rename from src/graphql/resolvers/queries/healthCheck.ts rename to src/resolvers/queries/healthCheck.ts index ab848dc..517eab5 100644 --- a/src/graphql/resolvers/queries/healthCheck.ts +++ b/src/resolvers/queries/healthCheck.ts @@ -1,4 +1,4 @@ -import type { GraphQLContext } from "../../context"; +import type { GraphQLContext } from "~/context"; export function healthCheck( _parent: unknown, diff --git a/src/resolvers/queries/home.ts b/src/resolvers/queries/home.ts new file mode 100644 index 0000000..f8bc2b8 --- /dev/null +++ b/src/resolvers/queries/home.ts @@ -0,0 +1,45 @@ +import { env } from "cloudflare:workers"; +import { graphql } from "gql.tada"; +import { GraphQLError } from "graphql"; + +import type { GraphQLContext } from "~/graph~/context"; +import { MediaFragment } from "~/types/title/mediaFragment"; + +enum HomeCategory { + WATCHING, + PLANNING, +} + +export async function home( + _parent: any, + args: { category: HomeCategory; page?: number }, + context: GraphQLContext, +) { + const { category, page = 1 } = args; + const { user, aniListToken } = context; + let statusFilters: string[] = []; + switch (category) { + case HomeCategory.WATCHING: + statusFilters = ["CURRENT"]; + break; + case HomeCategory.PLANNING: + statusFilters = ["PLANNING", "PAUSED", "REPEATING"]; + break; + } + + const stub = await env.ANILIST_DO.getByName("GLOBAL"); + const response = await stub.getTitles( + user?.name, + page, + statusFilters, + aniListToken, + ); + + if (!response) { + throw new GraphQLError(`Failed to fetch ${category} titles`, { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }); + } + + return response; +} diff --git a/src/graphql/resolvers/queries/popularBrowse.ts b/src/resolvers/queries/popularBrowse.ts similarity index 80% rename from src/graphql/resolvers/queries/popularBrowse.ts rename to src/resolvers/queries/popularBrowse.ts index 3c6010f..b5f6d2c 100644 --- a/src/graphql/resolvers/queries/popularBrowse.ts +++ b/src/resolvers/queries/popularBrowse.ts @@ -1,8 +1,7 @@ import { GraphQLError } from "graphql"; -import { fetchPopularTitlesFromAnilist } from "~/controllers/popular/browse/anilist"; - -import type { GraphQLContext } from "../../context"; +import type { GraphQLContext } from "~/context"; +import { fetchPopularTitlesFromAnilist } from "~/services/popular/browse/anilist"; interface PopularBrowseArgs { limit?: number; diff --git a/src/graphql/resolvers/queries/popularByCategory.ts b/src/resolvers/queries/popularByCategory.ts similarity index 75% rename from src/graphql/resolvers/queries/popularByCategory.ts rename to src/resolvers/queries/popularByCategory.ts index 9161411..c5018c9 100644 --- a/src/graphql/resolvers/queries/popularByCategory.ts +++ b/src/resolvers/queries/popularByCategory.ts @@ -1,9 +1,8 @@ import { GraphQLError } from "graphql"; -import { fetchPopularTitlesFromAnilist } from "~/controllers/popular/category/anilist"; -import type { PopularCategory } from "~/controllers/popular/category/enum"; - -import type { GraphQLContext } from "../../context"; +import type { GraphQLContext } from "~/context"; +import { fetchPopularTitlesFromAnilist } from "~/services/popular/category/anilist"; +import type { PopularCategory } from "~/services/popular/category/enum"; interface PopularByCategoryArgs { category: PopularCategory; diff --git a/src/graphql/resolvers/queries/search.ts b/src/resolvers/queries/search.ts similarity index 79% rename from src/graphql/resolvers/queries/search.ts rename to src/resolvers/queries/search.ts index 48590e5..bfbdc86 100644 --- a/src/graphql/resolvers/queries/search.ts +++ b/src/resolvers/queries/search.ts @@ -1,6 +1,5 @@ -import { fetchSearchResultsFromAnilist } from "~/controllers/search/anilist"; - -import type { GraphQLContext } from "../../context"; +import type { GraphQLContext } from "~/context"; +import { fetchSearchResultsFromAnilist } from "~/services/search/anilist"; interface SearchArgs { query: string; diff --git a/src/graphql/resolvers/queries/title.ts b/src/resolvers/queries/title.ts similarity index 92% rename from src/graphql/resolvers/queries/title.ts rename to src/resolvers/queries/title.ts index bf1999f..f081f6a 100644 --- a/src/graphql/resolvers/queries/title.ts +++ b/src/resolvers/queries/title.ts @@ -1,9 +1,8 @@ import { GraphQLError } from "graphql"; +import type { GraphQLContext } from "~/context"; import { fetchTitleFromAnilist } from "~/libs/anilist/getTitle"; -import type { GraphQLContext } from "../../context"; - interface TitleArgs { id: number; } diff --git a/src/resolvers/queries/user.ts b/src/resolvers/queries/user.ts new file mode 100644 index 0000000..f6dead2 --- /dev/null +++ b/src/resolvers/queries/user.ts @@ -0,0 +1,22 @@ +import { GraphQLError } from "graphql"; + +import type { GraphQLContext } from "~/context"; +import { getUser } from "~/services/auth/anilist/getUser"; + +export async function user(_parent: any, _args: {}, context: GraphQLContext) { + const { aniListToken } = context; + if (!aniListToken) { + throw new GraphQLError("Unauthorized", { + extensions: { code: "UNAUTHORIZED" }, + }); + } + + const response = await getUser(aniListToken); + if (!response) { + throw new GraphQLError(`Failed to fetch user`, { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }); + } + + return response; +} diff --git a/src/graphql/resolvers/title.ts b/src/resolvers/title.ts similarity index 84% rename from src/graphql/resolvers/title.ts rename to src/resolvers/title.ts index 0f935e6..c65d3c1 100644 --- a/src/graphql/resolvers/title.ts +++ b/src/resolvers/title.ts @@ -1,4 +1,4 @@ -import { fetchEpisodes } from "~/controllers/episodes/getByAniListId"; +import { fetchEpisodes } from "~/services/episodes/getByAniListId"; import type { Title as TitleType } from "~/types/title"; import { imageResolver } from "./image"; diff --git a/src/graphql/schema.ts b/src/schema.ts similarity index 100% rename from src/graphql/schema.ts rename to src/schema.ts diff --git a/src/controllers/auth/anilist/getUser.ts b/src/services/auth/anilist/getUser.ts similarity index 100% rename from src/controllers/auth/anilist/getUser.ts rename to src/services/auth/anilist/getUser.ts diff --git a/src/controllers/episodes/getByAniListId/aniwatch.ts b/src/services/episodes/getByAniListId/aniwatch.ts similarity index 100% rename from src/controllers/episodes/getByAniListId/aniwatch.ts rename to src/services/episodes/getByAniListId/aniwatch.ts diff --git a/src/services/episodes/getByAniListId/index.ts b/src/services/episodes/getByAniListId/index.ts new file mode 100644 index 0000000..d3b275f --- /dev/null +++ b/src/services/episodes/getByAniListId/index.ts @@ -0,0 +1,19 @@ +import { Episode } from "~/types/episode"; + +export async function fetchEpisodes( + aniListId: number, + shouldRetry: boolean = false, +): Promise { + // Check if we should use mock data + const { useMockData } = await import("~/libs/useMockData"); + if (useMockData()) { + const { mockEpisodes } = await import("~/mocks/mockData"); + return mockEpisodes(); + } + + return import("./aniwatch") + .then(({ getEpisodesFromAniwatch }) => + getEpisodesFromAniwatch(aniListId, shouldRetry), + ) + .then((episodeResults) => episodeResults?.episodes ?? []); +} diff --git a/src/controllers/episodes/getEpisodeUrl/aniwatch.ts b/src/services/episodes/getEpisodeUrl/aniwatch.ts similarity index 100% rename from src/controllers/episodes/getEpisodeUrl/aniwatch.ts rename to src/services/episodes/getEpisodeUrl/aniwatch.ts diff --git a/src/controllers/episodes/getEpisodeUrl/convertSkipTime.ts b/src/services/episodes/getEpisodeUrl/convertSkipTime.ts similarity index 100% rename from src/controllers/episodes/getEpisodeUrl/convertSkipTime.ts rename to src/services/episodes/getEpisodeUrl/convertSkipTime.ts diff --git a/src/controllers/episodes/getEpisodeUrl/index.spec.ts b/src/services/episodes/getEpisodeUrl/index.spec.ts similarity index 100% rename from src/controllers/episodes/getEpisodeUrl/index.spec.ts rename to src/services/episodes/getEpisodeUrl/index.spec.ts diff --git a/src/services/episodes/getEpisodeUrl/index.ts b/src/services/episodes/getEpisodeUrl/index.ts new file mode 100644 index 0000000..ae91755 --- /dev/null +++ b/src/services/episodes/getEpisodeUrl/index.ts @@ -0,0 +1,50 @@ +import { FetchUrlResponse } from "~/types/episode/fetch-url-response"; + +import { fetchEpisodes } from "../getByAniListId"; + +export async function fetchEpisodeUrl({ + id, + aniListId, + episodeNumber, +}: + | { id: string; aniListId?: number; episodeNumber?: number } + | { + id?: string; + aniListId: number; + episodeNumber: number; + }): Promise { + try { + let episodeId = id; + if (!id) { + const episodes = await fetchEpisodes(aniListId!); + if (episodes.length === 0) { + console.error(`Failed to fetch episodes for title ${aniListId}`); + return null; + } + const episode = episodes.find( + (episode) => episode.number === episodeNumber, + ); + if (!episode) { + console.error( + `Episode ${episodeNumber} not found for title ${aniListId}`, + ); + return null; + } + + episodeId = episode.id; + } + + const result = await import("./aniwatch").then( + ({ getSourcesFromAniwatch }) => getSourcesFromAniwatch(episodeId!), + ); + if (!result) { + return null; + } + + return result; + } catch (e) { + console.error("Failed to fetch download URL from Aniwatch", e); + + throw e; + } +} diff --git a/src/controllers/episodes/getEpisodeUrl/priorities.ts b/src/services/episodes/getEpisodeUrl/priorities.ts similarity index 100% rename from src/controllers/episodes/getEpisodeUrl/priorities.ts rename to src/services/episodes/getEpisodeUrl/priorities.ts diff --git a/src/controllers/episodes/markEpisodeAsWatched/anilist.ts b/src/services/episodes/markEpisodeAsWatched/anilist.ts similarity index 100% rename from src/controllers/episodes/markEpisodeAsWatched/anilist.ts rename to src/services/episodes/markEpisodeAsWatched/anilist.ts diff --git a/src/controllers/popular/browse/anilist.ts b/src/services/popular/browse/anilist.ts similarity index 100% rename from src/controllers/popular/browse/anilist.ts rename to src/services/popular/browse/anilist.ts diff --git a/src/controllers/popular/category/anilist.ts b/src/services/popular/category/anilist.ts similarity index 100% rename from src/controllers/popular/category/anilist.ts rename to src/services/popular/category/anilist.ts diff --git a/src/controllers/popular/category/enum.ts b/src/services/popular/category/enum.ts similarity index 100% rename from src/controllers/popular/category/enum.ts rename to src/services/popular/category/enum.ts diff --git a/src/controllers/popular/mapTitle.ts b/src/services/popular/mapTitle.ts similarity index 100% rename from src/controllers/popular/mapTitle.ts rename to src/services/popular/mapTitle.ts diff --git a/src/controllers/search/anilist.ts b/src/services/search/anilist.ts similarity index 68% rename from src/controllers/search/anilist.ts rename to src/services/search/anilist.ts index f5af34b..e5cf0f8 100644 --- a/src/controllers/search/anilist.ts +++ b/src/services/search/anilist.ts @@ -5,6 +5,23 @@ export async function fetchSearchResultsFromAnilist( page: number, limit: number, ): Promise { + // Check if we should use mock data + const { useMockData } = await import("~/libs/useMockData"); + if (useMockData()) { + const { mockSearchResults } = await import("~/mocks/mockData"); + + // Paginate mock results + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedResults = mockSearchResults.slice(startIndex, endIndex); + const hasNextPage = endIndex < mockSearchResults.length; + + return { + results: paginatedResults as any, + hasNextPage, + }; + } + const durableObjectId = env.ANILIST_DO.idFromName("GLOBAL"); const stub = env.ANILIST_DO.get(durableObjectId); diff --git a/src/controllers/watch-status/anilist.ts b/src/services/watch-status/anilist.ts similarity index 100% rename from src/controllers/watch-status/anilist.ts rename to src/services/watch-status/anilist.ts diff --git a/src/services/watch-status/index.ts b/src/services/watch-status/index.ts new file mode 100644 index 0000000..835243a --- /dev/null +++ b/src/services/watch-status/index.ts @@ -0,0 +1,22 @@ +import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; +import { buildNewEpisodeTaskId } from "~/libs/tasks/id"; +import { removeTask } from "~/libs/tasks/removeTask"; +import { setWatchStatus } from "~/models/watchStatus"; +import { WatchStatus } from "~/types/title/watchStatus"; + +export async function updateWatchStatus( + deviceId: string, + titleId: number, + watchStatus: WatchStatus | null, +) { + const { wasAdded, wasDeleted } = await setWatchStatus( + deviceId, + Number(titleId), + watchStatus, + ); + if (wasAdded) { + await maybeScheduleNextAiringEpisode(titleId); + } else if (wasDeleted) { + await removeTask("NEW_EPISODE", buildNewEpisodeTaskId(titleId)); + } +}