feat: associate device id with username when logging in

This commit is contained in:
2024-09-21 13:16:56 -04:00
parent 209a0b477d
commit c1bf12de4f
8 changed files with 1302 additions and 189 deletions

View File

@@ -5,6 +5,7 @@ import { streamSSE } from "hono/streaming";
import { fetchEpisodes } from "~/controllers/episodes/getByAniListId"; import { fetchEpisodes } from "~/controllers/episodes/getByAniListId";
import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode";
import { readEnvVariable } from "~/libs/readEnvVariable"; import { readEnvVariable } from "~/libs/readEnvVariable";
import { associateDeviceIdWithUsername } from "~/models/token";
import { setWatchStatus } from "~/models/watchStatus"; import { setWatchStatus } from "~/models/watchStatus";
import type { Env } from "~/types/env"; import type { Env } from "~/types/env";
import { EpisodesResponseSchema } from "~/types/episode"; import { EpisodesResponseSchema } from "~/types/episode";
@@ -60,9 +61,8 @@ const route = createRoute({
const app = new OpenAPIHono<Env>(); const app = new OpenAPIHono<Env>();
app.openapi(route, async (c) => { app.openapi(route, async (c) => {
// const deviceId = await c.req.header("X-Aniplay-Device-Id"); const deviceId = await c.req.header("X-Aniplay-Device-Id");
// const aniListToken = await c.req.header("X-AniList-Token"); const aniListToken = await c.req.header("X-AniList-Token");
const { deviceId, token: aniListToken } = await c.req.query();
if (!aniListToken) { if (!aniListToken) {
return c.json(ErrorResponse, { status: 401 }); return c.json(ErrorResponse, { status: 401 });
@@ -74,6 +74,8 @@ app.openapi(route, async (c) => {
return c.json(ErrorResponse, { status: 401 }); return c.json(ErrorResponse, { status: 401 });
} }
await associateDeviceIdWithUsername(env(c, "workerd"), deviceId, username);
return streamSSE( return streamSSE(
c, c,
async (stream) => { async (stream) => {

View File

@@ -118,3 +118,241 @@ exports[`requests the "/episodes" route with list of episodes from Anify 1`] = `
"success": true, "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,
}
`;

View File

@@ -440,3 +440,885 @@ exports[`requests the "/search" route valid query that returns anilist results 1
"success": true, "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,
}
`;

View File

@@ -77,3 +77,159 @@ The pages of Grimms' Fairy Tales, written by Jacob and Wilhelm, are now presente
"success": true, "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?"
<br><br>
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.
<br><br>
(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?"
<br><br>
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.
<br><br>
(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?"
<br><br>
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.
<br><br>
(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?"
<br><br>
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.
<br><br>
(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,
}
`;

View File

@@ -27,7 +27,7 @@ describe("requests the /token route", () => {
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
body: JSON.stringify({ token: "123", deviceId: "123", username: "test" }), body: JSON.stringify({ token: "123", deviceId: "123" }),
}); });
expect(res.json()).resolves.toEqual({ success: true }); expect(res.json()).resolves.toEqual({ success: true });
@@ -41,50 +41,7 @@ describe("requests the /token route", () => {
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
body: JSON.stringify({ token: "123", deviceId: "123", username: "test" }), 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: "test",
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("with username as null, 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", username: null }),
});
expect(res.json()).resolves.toEqual({ success: true });
expect(res.status).toBe(200);
});
it("with username as null, 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", username: null }),
}); });
const row = await db const row = await db
@@ -117,7 +74,7 @@ describe("requests the /token route", () => {
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
body: JSON.stringify({ token: "124", deviceId: "123", username: null }), body: JSON.stringify({ token: "124", deviceId: "123" }),
}); });
expect(res.json()).resolves.toEqual({ success: true }); expect(res.json()).resolves.toEqual({ success: true });
@@ -134,7 +91,7 @@ describe("requests the /token route", () => {
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
body: JSON.stringify({ token: "124", deviceId: "123", username: null }), body: JSON.stringify({ token: "124", deviceId: "123" }),
}); });
const row = await db const row = await db
@@ -157,23 +114,6 @@ describe("requests the /token route", () => {
).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second")); ).toBeGreaterThanOrEqual(+minimumTimestamp.startOf("second"));
}); });
it("token already exists in db, should fail", 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: "123", deviceId: "124", username: null }),
});
expect(res.json()).resolves.toEqual({ success: false });
expect(res.status).toBe(412);
});
it("token already exists in db, should not insert new entry", async () => { it("token already exists in db, should not insert new entry", async () => {
await db await db
.insert(deviceTokensTable) .insert(deviceTokensTable)
@@ -183,7 +123,7 @@ describe("requests the /token route", () => {
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
body: JSON.stringify({ token: "123", deviceId: "124", username: null }), body: JSON.stringify({ token: "123", deviceId: "124" }),
}); });
const row = await db const row = await db
@@ -195,64 +135,6 @@ describe("requests the /token route", () => {
expect(row).toBeUndefined(); expect(row).toBeUndefined();
}); });
it("associating a username with a token, 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: "123",
deviceId: "123",
username: "aniplay",
}),
});
expect(res.json()).resolves.toEqual({ success: true });
expect(res.status).toBe(200);
});
it("associating a username with a token should update existing entry", 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: "123",
deviceId: "123",
username: "aniplay",
}),
});
const row = await db
.select()
.from(deviceTokensTable)
.where(eq(deviceTokensTable.deviceId, "123"))
.get();
expect(row).toEqual({
deviceId: "123",
token: "123",
username: "aniplay",
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 is invalid, should fail", async () => { it("token is invalid, should fail", async () => {
mock.module("src/libs/fcm/verifyFcmToken", () => ({ mock.module("src/libs/fcm/verifyFcmToken", () => ({
verifyFcmToken: () => false, verifyFcmToken: () => false,
@@ -263,7 +145,7 @@ describe("requests the /token route", () => {
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
body: JSON.stringify({ token: "123", deviceId: "124", username: null }), body: JSON.stringify({ token: "123", deviceId: "124" }),
}); });
expect(res.json()).resolves.toEqual({ success: false }); expect(res.json()).resolves.toEqual({ success: false });
@@ -279,7 +161,7 @@ describe("requests the /token route", () => {
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json", "Content-Type": "application/json",
}), }),
body: JSON.stringify({ token: "123", deviceId: "124", username: null }), body: JSON.stringify({ token: "123", deviceId: "124" }),
}); });
const row = await db const row = await db

View File

@@ -3,7 +3,6 @@ import { env } from "hono/adapter";
import mapKeys from "lodash.mapkeys"; import mapKeys from "lodash.mapkeys";
import { Case, changeStringCase } from "~/libs/changeStringCase"; import { Case, changeStringCase } from "~/libs/changeStringCase";
import { TokenAlreadyExistsError } from "~/libs/errors/TokenAlreadyExists";
import type { AdminSdkCredentials } from "~/libs/fcm/getGoogleAuthToken"; import type { AdminSdkCredentials } from "~/libs/fcm/getGoogleAuthToken";
import { verifyFcmToken } from "~/libs/fcm/verifyFcmToken"; import { verifyFcmToken } from "~/libs/fcm/verifyFcmToken";
import { readEnvVariable } from "~/libs/readEnvVariable"; import { readEnvVariable } from "~/libs/readEnvVariable";
@@ -21,7 +20,6 @@ const app = new OpenAPIHono<Env>();
const SaveTokenRequest = z.object({ const SaveTokenRequest = z.object({
token: z.string(), token: z.string(),
deviceId: z.string(), deviceId: z.string(),
username: z.string().nullable(),
}); });
const SaveTokenResponse = SuccessResponseSchema(); const SaveTokenResponse = SuccessResponseSchema();
@@ -70,8 +68,7 @@ const route = createRoute({
}); });
app.openapi(route, async (c) => { app.openapi(route, async (c) => {
const { token, deviceId, username } = const { token, deviceId } = await c.req.json<typeof SaveTokenRequest._type>();
await c.req.json<typeof SaveTokenRequest._type>();
try { try {
const isValidToken = await verifyFcmToken( const isValidToken = await verifyFcmToken(
@@ -85,12 +82,8 @@ app.openapi(route, async (c) => {
return c.json(ErrorResponse, 401); return c.json(ErrorResponse, 401);
} }
await saveToken(env(c, "workerd"), deviceId, token, username); await saveToken(env(c, "workerd"), deviceId, token);
} catch (error) { } catch (error) {
if (error instanceof TokenAlreadyExistsError) {
return c.json(ErrorResponse, 412);
}
console.error(new Error("Failed to save token", { cause: error })); console.error(new Error("Failed to save token", { cause: error }));
return c.json(ErrorResponse, 500); return c.json(ErrorResponse, 500);
} }

View File

@@ -1,5 +0,0 @@
export class TokenAlreadyExistsError extends Error {
constructor() {
super("Token already exists in the database");
}
}

View File

@@ -1,68 +1,33 @@
import { and, eq, gt, or, sql } from "drizzle-orm"; import { and, eq, gt, sql } from "drizzle-orm";
import { TokenAlreadyExistsError } from "~/libs/errors/TokenAlreadyExists";
import type { Env } from "~/types/env"; import type { Env } from "~/types/env";
import { getDb } from "./db"; import { getDb } from "./db";
import { deviceTokensTable, watchStatusTable } from "./schema"; import { deviceTokensTable, watchStatusTable } from "./schema";
export function saveToken( export function saveToken(env: Env, deviceId: string, token: string) {
env: Env, return insertToken(env, deviceId, token);
deviceId: string,
token: string,
username: string | null,
) {
return getDb(env)
.select()
.from(deviceTokensTable)
.where(
or(
eq(deviceTokensTable.deviceId, deviceId),
eq(deviceTokensTable.token, token),
),
)
.then((existingTokens) => {
const existingToken = existingTokens.find(
({ token: existingToken, deviceId: existingDeviceId }) =>
existingToken === token || existingDeviceId === deviceId,
);
if (!existingToken) {
return insertToken(env, deviceId, token, username);
}
if (
existingToken.token === token &&
!existingToken.username &&
!username
) {
throw new TokenAlreadyExistsError();
}
return updateToken(env, deviceId, token, username);
});
} }
function insertToken( function insertToken(env: Env, deviceId: string, token: string) {
env: Env,
deviceId: string,
token: string,
username: string | null,
) {
return getDb(env) return getDb(env)
.insert(deviceTokensTable) .insert(deviceTokensTable)
.values({ deviceId, token, username }) .values({ deviceId, token })
.onConflictDoUpdate({
set: { token },
target: [deviceTokensTable.deviceId],
})
.run(); .run();
} }
function updateToken( export function associateDeviceIdWithUsername(
env: Env, env: Env,
deviceId: string, deviceId: string,
token: string, username: string,
username: string | null,
) { ) {
return getDb(env) return getDb(env)
.update(deviceTokensTable) .update(deviceTokensTable)
.set({ token, username }) .set({ username })
.where(eq(deviceTokensTable.deviceId, deviceId)) .where(eq(deviceTokensTable.deviceId, deviceId))
.run(); .run();
} }