From 68c082493e0fd4fe7b1dc0ccba11592d18177f43 Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Wed, 15 May 2024 23:03:08 -0400 Subject: [PATCH] feat: create route to return title information Summary: Test Plan: --- README.md | 4 + bun.lockb | Bin 75701 -> 102045 bytes package.json | 3 + src/controllers/health-check/index.ts | 5 +- src/controllers/title/amvstrm.ts | 76 ++++ src/controllers/title/anilist.ts | 32 ++ src/controllers/title/index.spec.ts | 127 ++++++ src/controllers/title/index.ts | 61 +++ src/controllers/title/mediaFragment.ts | 35 ++ src/index.ts | 4 + src/mocks/amvstrm/title.ts | 602 +++++++++++++++++++++++++ src/mocks/anify/title.ts | 12 + src/mocks/anilist/title.ts | 70 +++ src/mocks/handlers.ts | 6 +- src/types/schema.ts | 8 + src/types/title/countryCodes.ts | 253 +++++++++++ src/types/title/index.ts | 60 +++ tsconfig.json | 13 +- 18 files changed, 1367 insertions(+), 4 deletions(-) create mode 100644 src/controllers/title/amvstrm.ts create mode 100644 src/controllers/title/anilist.ts create mode 100644 src/controllers/title/index.spec.ts create mode 100644 src/controllers/title/index.ts create mode 100644 src/controllers/title/mediaFragment.ts create mode 100644 src/mocks/amvstrm/title.ts create mode 100644 src/mocks/anify/title.ts create mode 100644 src/mocks/anilist/title.ts create mode 100644 src/types/title/countryCodes.ts create mode 100644 src/types/title/index.ts diff --git a/README.md b/README.md index 7dce3b3..43386ab 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,7 @@ pre-commit hook for Sapling (`.sl/config`): [hooks] precommit = echo $HG_PARENT1 && bun prettier $(sl show -T "{file_adds} {file_mods}\n\n" $HG_PARENT1 --stat | head -n 1) --write ``` + +## Development + +If a route is internal-only or doesn't need to appear on the OpenAPI spec (that's autogenerated by Hono), use the `Hono` class. Otherwise, use the `OpenAPIHono` class from `@hono/zod-openapi`. diff --git a/bun.lockb b/bun.lockb index 7e88c6e1ae15d553b53d7bb3482df6df65fb9ebe..46b2da3b5f6f74e6478c5e767769d41c5bf16639 100755 GIT binary patch delta 30555 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 a$*t2VNORr_T45nrYCeAs~DtDl$ zs2R?gDWW(~*;Uij+jA{ZdqS^fHyiAxW!>!k{`R2W``YvN-1qaoe?9x-cm39P&A(x- zy^k*I>mB!(vu1F zd?3_3?Qa+PzECr}du2HaJOrU?FzP%Z!yx@3??Adh&bAg7P^Xz?)}ko|wlY2PJ}A#C zw>@VoswjWnP7rj|u6RbyRMdTIuk=fD5QGjWPeVP)O7KpQ*7Ar7>*NC47)L?y5(K`; zF0eA*S((!t(id&+ISE2{$jgv^kS&k_kTZ&M^KA!Gyw#hT5Ko*j+cov}@t;w2GTvj9qmBKU>&_utwD$31)mfgT}A-h73 zffBT^!u&!T4KM_pj1#9SHvp@l0b?M^n5QAB-osrGFl1FQ+R=i`Y~{rT&)J0gC?^Gf zfb4+zt8O4e8cem87th3!l3*&#UDX90sl3QqRGgnH2)6RcGx7^^g}X2vS@RksdF?DD z{!|@S%eSh0H6+zntK}tXc{XGhln*9eiA)qSq^XZu;Rs0`zej>>)#s2j;c1ochok{E zLE=x<3M8E&pNFLOxsX(!sq&nu#nbb1tz~64Yi@3SS;=*nmt40VvXV5+FOQf~P&`@K z2s@K0YfwSuec%@|=|XTa>EtqNj;&k}?xCF8r6X9WT@54+Xf2vuXsyVZ>Y}>nx+n;t zC~t+m$%SbF$^s8Ul0ppuus@l47YfK%YXTKt?L-IC6#aCNogit!&X81oFIXAx zOGt!W)s)IIYsu8o0wHIrwO~31&g=ak7$foutW(<54rd_AQZ10Qu&t1AZ-g7#ML|wHyl}3r&L+a-gMlpw*@-3Rb5n+f-{OpU&7>RcI?e z5sBBn8ZxM|b!K76(eRzapW>M=KMftc{>rwET@U@VV!jaHZaDS9=6Bxz%f3%m4h?Ma z`eM|L^bHq(T$y`t!(VUvq;(Jzoo08KeS6=hm(pA2-t(|czaM|5cG9pMmmW*9+BTKA ziq7jps`~U^V4KWKIyO4Dc1pQ3G2@jFxwl(@bGJcdKj-u~=EvW1_uiZ77Ra0i4!-1; z_H`03abM-F4Z5at)87mWTBf((KeH<=UjOEbnkXNQ<{!F*C?7qo*ilu7mB42BQ_3+!nWf41jsJx$^O2W|*6i8T&9JItgz;~)qbXd?0kgHiNx zxL6;sLa^}<*eNi0Ul~P==)miGn{=hv2{g3GtzC?|U0{9XCf3eIhnrx7_?6%kT_2nz zR4$U)#5@#_T&?GI-Av+_dYBdI2iJ{pHGUzeB@I3 zc%KyUs0YuEG3o4ac97*|AL*U|8z7Hwg*uBmaYL+0yx56nLwI}gx>%EgGu~`Z@cP&k zhY`pPk#nyiH%!i5c~IM$Dga9a!~W@J)a?Kx2PuJdQ*8;>*{DnN6NKS%T?2w{ z5g57Ify2}e$HC%xy&=WH4)5+%e#Ik2Hv&1D$APzd7>Au3jPj!ew52Kh9#OqMECWyD0OuB19f?$-V zqrDUwEC`WuODHKuh48v`ldcXg7&Xtg>XYZlgFnPtj!|M~9WOJo+lI z2c~rR3z&)5cS+F&ge%dnXju@>vj>{Q#&B-vY!a`9^SXi9xV@BRGH%5gSPVwfVXGou z_JIurlOsX=wHG&ZGU?*+{XjF@lbdxFVB}IKxx)c43OCu}y6?c!!Q{2;qHwCDfZ;^M z(9^&uSmZzw8~bp>V3W=tpBiL993oJC0vMg@*vRPcHW(dnSZ8OW_-zC?3^D1KtWO07mm+)i@Ei5u@WncT;U8my123dD~EvZgRBdNE~M3#%P{B z%%r=5LUOs{GyfQDR4{1cYt-d{(ZX@CBd|AtWjtWtf{{&RALx3*bYxPTz}RZVU=7ni8MZVn!-ZQzU;r@eeN_LJ$PFV+Vu6txf=v#Mi2EYByh{?V zLwQ*eZ-cm)#0{fNqE9l<9%a(aOx9us>N@NN8$f+@_mQIo$?kVZLN=C<4^7ccLym$3 z!5@T|6PR))!2}LBz%uz2Ly9gD(LpYPf$?&xNa1y(O}hOkq+Nu7Ofibzr*Oj4g?dy^Qd$MM`Q?g09+-H1KZ0I#*`n(N-ZM~4E5#bagr7Sn+W*Spxq-s ze?-!P`zg&IBbE8(3YuVm+K?o59H{cgNvenUWQ}k($j_sa>W8T1k4lR)-pCpyV(v!sSkR*Oel^;UVSZ7rJ5hMlA1%RGMCAIrhF8ycO0cBU!a*|}c>neww zCTFRDCcdG{FCb~aF9GUsOXc4{(nHc7_?`$JlGN@FK;zs6=y_B!?q}^@Nxl9d*C~=_ zxeripMqO1&++O8W=AuH&Ym&zMZye}Ka0OFuJS6|%i7vON6Q0)exPzP;P&p~|`vO9rnuE%_0C`04{6CWa^}+n_PW;CYbl9Ixb~5FE??e}bf9pWUJmhkkuL=5iX8cU0nGdz;c}SI+Iq=dd3v=Wrk>alk=2@6CpN&+<&m--?!(Xy67d{WEE5C?T z&tv9W_<^Z<{@Q#qbK~t`Q}gxwiE1DOgsGnWgZC8t6Am&mCSd zvot>X6$>-*tw_^3;}+JBk3-s@??F0%>t406f!vC85N}5M1owE&!Upqvq(gWs(xKdE zwS^7irAUYKlSs`xP_nQLJ{#!>eje#a9{#$8jpFl=X7Y4qgjfFkUYmtuQx7R?Am!QX5Gkb64vvu$jm~EYzS@|BY*B8Kt>&+~OTi3%+3*kdB8~4~?VR<|s=@j0IbSn30u&{hy z+5jIdhQ}JrY&x$gf{&KKV;jw^kk8%-AAwx~E9T*w;G?B_zI>CJmGX;V-pg=0Y&NrU zzGO2r1N$0m22Xwin!T*&^>3KjO#UTU*m6A|vBk`0^V%)Y4eVF2Iegfg&~1gD?|jqD zUgY<{l2+>Z_(n6U8pna$(*`Os|@GzFW__iTl3HPCdMnJwVfZHPgzESs4v z;?3JEY%%xPZedG!KGLPU73nhWv%|t(=A}rN^OHzd@W7oGwvx|Ax{9AiTEoM4S=cLl z9#YOPB7K#|?6$Di_!6Y6c{@^xC%^c^Zi0>9Yi136>t1NG270|?W}Eo9cc95yJwFWg2G{L_CSZm8%5TA$4teMvwf~Ual95RcC1U{f; zEML4CTdl<`9v1lbph<7Q=7-JVQGstfJeJ=CbvR;Xt^DK+sJI1zgN^?l51$DY--P-{ z%;Ir@Z#%M-R~^mdnI86h!*MVESKmH7z{8ceUi)WyKb*C${Pz1RdGrYf*BSU1vz)26 zCPymlerg}=g~X{EiFKi(hWQu02U;#qrKkQ$Pir!?P@g;n1FO?jy106^o>FCKa{ zPD?x|mk2^LByl_7l3I2MlKRnG=(1YYqLw*8UQx>qt7R~j{F@b`QaGX(Izd7=`8kS| z2Bc3s`V~aaF@VZ+0R2*;rxl39_!u|^R09iug}@?U zF|Y(!3M`YB>sYs#SCHkvtH3@J|6*z*xWnJPnKkvVmuSXMyp+1YjaC39tf_fgFm3Tx4v3 z8OQ)e07HPGz+iylBLoNqdIDiUIM55|4fFvbfJndtxCML-+yj0Fz5uQQ1AyTGeMKGs z4g!bho1G$*0+Iso7|;q(K)nfU2kL=!!1KU!Kp8+kE$A8}JWc1JH>1<-Z_$0rEwl92f<}0r3FUR{%x;aVyv{LAVI{pTJGv25=qN z1PlZy`o97`0KNzQ4*USz1?~Vp0^SrBc*4#If^^QA1;psUHUkX+sa6P(v!?^jfE^$L zm|Xt*Q+sKi8}qDmREwM-DMls(R^Um1tU%TvtB@k35V^<=Aa{}LT!0P$O;4^31<2Kb zKmgDk@CST>E_wqd|fUH0JQ*V@H!v?tAW=5n*3GZ(aGz;so#3Q3)ragO{#n!au@l38!~SK zTYxtJ(w@w@6=($Z0!M)(z#iZ&U^lP}*Z~9q+ku^c)`!XoZ>y5V(Aw`q{vCkYHcddX zUoAKUc@Q`NGy^TbVSomD7ia~J0lk25;5}7RIVo@qI1W601__>HUkH-LlZ7PcL44qf zrAM3HFI~R2z?o4sN~=O4Lm;mdj=QyReyY`(O^A<-i^SeBOJ_aV2$m)Jc`+ZBFB!d9 z94nWmgGmRxSiBgx~5;QO!k^J4R{0>FeUP=vSi^Xs4r4NExY@&8+ z^lJLmFAfabaz@iBR?zN`8e5m`Up=s0(-uC0wvi#sJ5jw+T6H5Z-?XU5)&NsLO-~J`q9;tiSx^j zUwc@i-BLYp-jI{qEWl==8-S5LLOQ_uzxNdYuRFJ&577wJ?e^G(!loL&z4deph(^+#o;<9%IpI`80gvw~H_5jr^Y+zlr|ODRKlt0|xpYtsG9$l z?QE~)QO&AJ@e;Z3E>-s=hjmgyU`_2QLuugw7r8|YMUr;&^Mq5TH27xWyY>jN7-M8i zvXJX3-J;R8yQ1Dd{~R;sRHE%+ixr+yKp0%A-7bwDP&cv3(M^2VLc4#e?>gwqb-SMX z)5Dt6o>CF@{oGTz!`TF}>5y9K*_aQ^tR z^qy-UwuthQzN69Qi?i@ruTEicY}vJjhb^?LwFQ$l{&u0Jx&OnO5--UVj=tJu+%HyF zeBY3M_t3)@+I8K<_B*Cd^yoY5Va--A>1Fiw)h_t9Uax8N>^kJfhb_)}Nyn(gH5~I$ zy~^3c)cXK?_4LCQcfF(^!dZr|c9nMB!b1Zaf9<$M_Pgw}nr_mwy|9tjcav82Vj1G~ zZqjGHFs61Xw?lnWUKf$0u#N38e-$MI{@UVftpH5BTo!Th_>*H^p#xT4|htY2R| zn}rs%fzhJ1yL6T2PYaOWZ&jIpP6$ozeQWr`F&YA-=suW#7h1Stmeu>so1BgB|A-bi zMia234x$EOT(!IO`Zqrf`uZQJiH%Gm?bN>Fnn3CGJ}lNtyXzYg-?aSfc-Jl>O9_%b z>%#)XPlHxRU=r=-scl#06BT`r2FXLo3)SwZ{!*XPGc3;JhZ^-PUK%Woq`um<*}G@B zly2-bL~emLct-{+F1^>_`6s_3yH?a->B*>S36|EPudjAB_bbz}^*!e{-;!G-Qs1^< z>0AU{`DL)=kC%$CcKbJU>*b%vdX_GgXTciy3a$i_9 zEtpy&ZeUyOmh-LL?5r!P$LO?9Qd~MVR60Uq6oyI}aai$Qyq@Vjm(A8*8vNt>60}TE zLJPCFMn7yRE((+S5kD0sRZ-8E@KPt2|60J_D(Nw(4L##AIdsL?E6_6W!JX;S;Iw`& zZb`I0Wo+#h_4%i_og4PU`s>&vwD5TRKn{~G(=2(trMuD4QM>lsW7x2$;Q~r;h^iI?+ zTyM*&3feX#Y5~Sb!WfueyOOU+rf3oLf`QEbjDG50O!j zBq=Z1%y`70cISN4hp*3GKkd)?h@+&)gvgj^?KLbekCWD+WukV8+)cgpj5Qh~W1}Mz z1nv5{;itl{x0-uk&!R&9Tx*V#ZlSMuJWg^yzHr8I_JKvwMY=O3-NB-@9&sV7u->+NfheiBvT?*4h>cr znU_?M$dY{jnyB2T4>`W&$)4S&*(=SlX9Xy749$IiUlC=XF7^qyoi7g=5DU|3H}40G z${ICdIX|K;ASOX=&g4@!Gpx>Y&cOIu;AP?#+B zNrFP^N~OC-X7JVS_1|n<>UQCRBb{C9i%V_ptKH}y8TIZ)pHuJAfbcBs#GT!xNi-nN zndg(>ThDN57r5{Lelk(a!Qtv6$pp2j(zAZd8y}OMlUdy3KPKgaQ2m&+j~A-bq*dr5 zswMu>5B~FjcJm%flYb?|(?r9Qu_SzY&rN3DF77umJDycH<-eRwAFNRA|BPK7G_9h} zX=k6^Kq`$SeLogdCD z(zb~#RbrFy`>oZ?hDlMAn1ke=!MaM*hhT(P%8^_)GgoQm5auSel%rzVFczqkjm%)_ zO_`I}m`*;n@`^H>En+7A$BeD4&^jeQCjuq;MN^tKma#9H)b$CLBl*u@&P}^2U{uG7 z*(J7!JgGE=`8Tyc$AXwWHJ9|yW9;s;kofgyX-(y`*vF#u(L8+Jf6|YIHw~Y|x-h9O zl^L5Ry}+`Z9ZIZa<+i4s)ohoYbZ`tSl)5iw9i@qjSl6bti`iTDUHeTd%g>!+n`tX3 yh?-(6iYT;}lweg-#&YO>1^?a!d_H(Qsp%l`|+!ML#i diff --git a/package.json b/package.json index 4729e9d..9bc6da3 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ }, "dependencies": { "@hono/zod-openapi": "^0.12.0", + "gql.tada": "^1.7.4", + "graphql-request": "^7.0.1", "hono": "^4.3.6", "zod": "^3.23.8" }, "devDependencies": { + "@0no-co/graphqlsp": "^1.12.3", "@cloudflare/workers-types": "^4.20240403.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/bun": "^1.1.2", diff --git a/src/controllers/health-check/index.ts b/src/controllers/health-check/index.ts index c1be941..9870299 100644 --- a/src/controllers/health-check/index.ts +++ b/src/controllers/health-check/index.ts @@ -7,6 +7,9 @@ const app = new OpenAPIHono(); const route = createRoute({ method: "get", path: "/", + summary: "Health check", + operationId: "healthCheck", + tags: ["aniplay"], responses: { 200: { content: { @@ -14,7 +17,7 @@ const route = createRoute({ schema: SuccessResponseSchema(), }, }, - description: "Retrieve the user", + description: "Server is up and running!", }, }, }); diff --git a/src/controllers/title/amvstrm.ts b/src/controllers/title/amvstrm.ts new file mode 100644 index 0000000..ee65ecc --- /dev/null +++ b/src/controllers/title/amvstrm.ts @@ -0,0 +1,76 @@ +import { Title } from "~/types/title"; + +export async function fetchTitleFromAmvstrm( + aniListId: number, +): Promise { + return Promise.all([ + fetch(`https://api-amvstrm.nyt92.eu.org/api/v2/info/${aniListId}`).then( + (res) => res.json() as Promise<any>, + ), + fetchMissingInformationFromAnify(aniListId).catch((err) => { + console.error("Failed to get missing information from Anify", err); + return null; + }), + ]).then( + async ([ + { + id, + idMal, + title: { english: englishTitle, userPreferred: userPreferredTitle }, + description, + episodes, + genres, + status, + bannerImage, + coverImage: { + extraLarge: extraLargeCoverImage, + large: largeCoverImage, + medium: mediumCoverImage, + }, + countryOfOrigin, + nextair: nextAiringEpisode, + score: { averageScore }, + }, + anifyInfo, + ]) => { + return { + id, + idMal, + title: { + userPreferred: userPreferredTitle, + english: englishTitle, + }, + description, + episodes, + genres, + status, + averageScore, + bannerImage: bannerImage ?? anifyInfo?.bannerImage, + coverImage: { + extraLarge: extraLargeCoverImage, + large: largeCoverImage, + medium: mediumCoverImage, + }, + countryOfOrigin: countryOfOrigin ?? anifyInfo?.countryOfOrigin, + nextAiringEpisode, + mediaListEntry: null, + }; + }, + ); +} + +type AnifyInformation = { + bannerImage: string | null; + countryOfOrigin: string; +}; + +function fetchMissingInformationFromAnify( + aniListId: number, +): Promise<AnifyInformation> { + return fetch(`https://api.anify.tv/info?id=${aniListId}`) + .then((res) => res.json() as Promise<AnifyInformation>) + .then(({ bannerImage, countryOfOrigin }) => ({ + bannerImage, + countryOfOrigin, + })); +} diff --git a/src/controllers/title/anilist.ts b/src/controllers/title/anilist.ts new file mode 100644 index 0000000..fb8fd21 --- /dev/null +++ b/src/controllers/title/anilist.ts @@ -0,0 +1,32 @@ +import { graphql } from "gql.tada"; +import { GraphQLClient } from "graphql-request"; + +import type { Title } from "~/types/title"; + +import { MediaFragment } from "./mediaFragment"; + +const GetTitleQuery = graphql( + ` + query GetTitle($id: Int!) { + Media(id: $id) { + ...Media + } + } + `, + [MediaFragment], +); + +export async function fetchTitleFromAnilist( + id: number, + token: string | undefined, +): Promise<Title | undefined> { + const client = new GraphQLClient("https://graphql.anilist.co/"); + const headers = new Headers(); + if (token) { + headers.append("Authorization", `Bearer ${token}`); + } + + return client + .request(GetTitleQuery, { id }, headers) + .then((data) => data?.Media ?? undefined); +} diff --git a/src/controllers/title/index.spec.ts b/src/controllers/title/index.spec.ts new file mode 100644 index 0000000..9a709a8 --- /dev/null +++ b/src/controllers/title/index.spec.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "bun:test"; + +import app from "~/index"; +import { server } from "~/mocks"; + +server.listen(); + +describe('requests the "/title" route', () => { + it("with a valid id & token", async () => { + const response = await app.request("/title?id=10", { + headers: new Headers({ "x-anilist-token": "asd" }), + }); + + expect(response.json()).resolves.toEqual({ + success: true, + result: { + nextAiringEpisode: null, + mediaListEntry: { + status: "CURRENT", + progress: 1, + id: 402665918, + }, + countryOfOrigin: "JP", + coverImage: { + medium: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + large: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + extraLarge: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg", + }, + averageScore: 66, + bannerImage: + "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + status: "FINISHED", + genres: ["Fantasy", "Thriller"], + episodes: 6, + description: + 'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)', + title: { + userPreferred: "The Grimm Variations", + english: "The Grimm Variations", + }, + idMal: 49210, + id: 135643, + }, + }); + expect(response.status).toBe(200); + }); + + it("with a valid id but no token", async () => { + const response = await app.request("/title?id=10"); + + expect(response.json()).resolves.toEqual({ + success: true, + result: { + nextAiringEpisode: null, + mediaListEntry: null, + countryOfOrigin: "JP", + coverImage: { + medium: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + large: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + extraLarge: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg", + }, + averageScore: 66, + bannerImage: + "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + status: "FINISHED", + genres: ["Fantasy", "Thriller"], + episodes: 6, + description: + 'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)', + title: { + userPreferred: "The Grimm Variations", + english: "The Grimm Variations", + }, + idMal: 49210, + id: 135643, + }, + }); + expect(response.status).toBe(200); + }); + + it("with an unknown title from anilist but valid title from amvstrm", async () => { + const response = await app.request("/title?id=50"); + + expect(response.json()).resolves.toEqual({ + success: true, + result: { + nextAiringEpisode: null, + mediaListEntry: null, + coverImage: { + medium: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-yxY3olrjZH4k.png", + large: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx151807-yxY3olrjZH4k.png", + }, + averageScore: 83, + bannerImage: + "https://s4.anilist.co/file/anilistcdn/media/anime/banner/151807-37yfQA3ym8PA.jpg", + status: "FINISHED", + genres: ["Action", "Adventure", "Fantasy"], + episodes: 12, + description: + "They say whatever doesn’t kill you makes you stronger, but that’s not the case for the world’s weakest hunter Sung Jinwoo. After being brutally slaughtered by monsters in a high-ranking dungeon, Jinwoo came back with the System, a program only he could see, that’s leveling him up in every way. Now, he’s inspired to discover the secrets behind his powers and the dungeon that spawned them.<br>\n<br>\n(Source: Crunchyroll) <br><br>", + title: { + userPreferred: "Ore dake Level Up na Ken", + english: "Solo Leveling", + }, + idMal: 52299, + id: 151807, + countryOfOrigin: "JP", + }, + }); + expect(response.status).toBe(200); + }); + + it("with an unknown title from all sources", async () => { + const response = await app.request("/title?id=-1"); + + expect(response.json()).resolves.toEqual({ success: false }); + expect(response.status).toBe(404); + }); +}); diff --git a/src/controllers/title/index.ts b/src/controllers/title/index.ts new file mode 100644 index 0000000..d3a1d66 --- /dev/null +++ b/src/controllers/title/index.ts @@ -0,0 +1,61 @@ +import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; + +import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; +import { + AniListIdSchema, + ErrorResponseSchema, + SuccessResponseSchema, +} from "~/types/schema"; +import { Title } from "~/types/title"; + +import { fetchTitleFromAmvstrm } from "./amvstrm"; +import { fetchTitleFromAnilist } from "./anilist"; + +const app = new OpenAPIHono(); + +const route = createRoute({ + tags: ["aniplay", "title"], + operationId: "fetchTitle", + summary: "Fetch title information", + method: "get", + path: "/", + request: { + query: z.object({ id: AniListIdSchema }), + headers: z.object({ "x-anilist-token": z.string().nullish() }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: SuccessResponseSchema(Title), + }, + }, + description: "Returns title information", + }, + "404": { + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + description: "Title could not be found", + }, + }, +}); + +app.openapi(route, async (c) => { + const aniListId = Number(c.req.query("id")); + const aniListToken = c.req.header("X-AniList-Token"); + + const title = await fetchFromMultipleSources([ + () => fetchTitleFromAnilist(aniListId, aniListToken ?? undefined), + () => fetchTitleFromAmvstrm(aniListId), + ]); + if (!title) { + return c.json({ success: false }, 404); + } + + return c.json({ success: true, result: title }, 200); +}); + +export default app; diff --git a/src/controllers/title/mediaFragment.ts b/src/controllers/title/mediaFragment.ts new file mode 100644 index 0000000..edd62e0 --- /dev/null +++ b/src/controllers/title/mediaFragment.ts @@ -0,0 +1,35 @@ +import { graphql } from "gql.tada"; + +export const MediaFragment = graphql(` + fragment Media on Media { + id + idMal + title { + english + userPreferred + } + type + description + episodes + genres + status + bannerImage + averageScore + coverImage { + extraLarge + large + medium + } + countryOfOrigin + mediaListEntry { + id + progress + status + } + nextAiringEpisode { + timeUntilAiring + airingAt + episode + } + } +`); diff --git a/src/index.ts b/src/index.ts index 7048a4e..206aa92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,10 @@ app.route( (controller) => controller.default, ), ); +app.route( + "/title", + await import("~/controllers/title").then((controller) => controller.default), +); // The OpenAPI documentation will be available at /doc app.doc("/doc", { diff --git a/src/mocks/amvstrm/title.ts b/src/mocks/amvstrm/title.ts new file mode 100644 index 0000000..97e2105 --- /dev/null +++ b/src/mocks/amvstrm/title.ts @@ -0,0 +1,602 @@ +import { HttpResponse, http } from "msw"; + +export function getAmvstrmTitle() { + return http.get( + "https://api-amvstrm.nyt92.eu.org/api/v2/info/:aniListId", + ({ params }) => { + const aniListId = Number(params["aniListId"] as string); + + if (aniListId == -1) { + return HttpResponse.json({ + code: 404, + message: + "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + }); + } + + if (aniListId == 50) { + return HttpResponse.json({ + code: 200, + message: "success", + id: 151807, + idMal: 52299, + id_provider: { + idGogo: "ore-dake-level-up-na-ken", + idGogoDub: "ore-dake-level-up-na-ken-korean-dub", + idZoro: "solo-leveling-18718", + id9anime: "solo-leveling.3rpv2", + idPahe: "5421", + }, + title: { + romaji: "Ore dake Level Up na Ken", + english: "Solo Leveling", + native: "俺だけレベルアップな件", + userPreferred: "Ore dake Level Up na Ken", + }, + dub: true, + description: + "They say whatever doesn’t kill you makes you stronger, but that’s not the case for the world’s weakest hunter Sung Jinwoo. After being brutally slaughtered by monsters in a high-ranking dungeon, Jinwoo came back with the System, a program only he could see, that’s leveling him up in every way. Now, he’s inspired to discover the secrets behind his powers and the dungeon that spawned them.<br>\n<br>\n(Source: Crunchyroll) <br><br>", + coverImage: { + large: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx151807-yxY3olrjZH4k.png", + medium: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx151807-yxY3olrjZH4k.png", + color: "#35bbf1", + }, + bannerImage: + "https://s4.anilist.co/file/anilistcdn/media/anime/banner/151807-37yfQA3ym8PA.jpg", + genres: ["Action", "Adventure", "Fantasy"], + tags: [ + { + id: 604, + name: "Dungeon", + }, + { + id: 82, + name: "Male Protagonist", + }, + { + id: 321, + name: "Urban Fantasy", + }, + { + id: 66, + name: "Super Power", + }, + { + id: 29, + name: "Magic", + }, + { + id: 1243, + name: "Necromancy", + }, + { + id: 326, + name: "Cultivation", + }, + { + id: 111, + name: "War", + }, + { + id: 104, + name: "Anti-Hero", + }, + { + id: 94, + name: "Gore", + }, + { + id: 636, + name: "Cosmic Horror", + }, + { + id: 85, + name: "Tragedy", + }, + { + id: 43, + name: "Swordplay", + }, + { + id: 56, + name: "Shounen", + }, + { + id: 146, + name: "Alternate Universe", + }, + { + id: 96, + name: "Time Manipulation", + }, + { + id: 1068, + name: "Angels", + }, + { + id: 93, + name: "Post-Apocalyptic", + }, + { + id: 217, + name: "Dystopian", + }, + { + id: 1310, + name: "Travel", + }, + { + id: 1045, + name: "Heterosexual", + }, + { + id: 488, + name: "Age Regression", + }, + { + id: 244, + name: "Isekai", + }, + { + id: 171, + name: "Bullying", + }, + { + id: 224, + name: "Dragons", + }, + { + id: 255, + name: "Ninja", + }, + ], + status: "FINISHED", + format: "TV", + episodes: 12, + year: 2024, + season: "WINTER", + duration: 24, + startIn: { + year: 2024, + month: 1, + day: 7, + }, + endIn: { + year: 2024, + month: 3, + day: 31, + }, + nextair: null, + score: { + averageScore: 83, + decimalScore: 8.3, + }, + popularity: 196143, + siteUrl: "https://anilist.co/anime/151807", + trailer: { + id: "HkIKAnwLZCw", + site: "youtube", + thumbnail: "https://i.ytimg.com/vi/HkIKAnwLZCw/hqdefault.jpg", + }, + studios: [ + { + name: "A-1 Pictures", + }, + { + name: "Aniplex", + }, + { + name: "Netmarble", + }, + { + name: "D&C MEDIA", + }, + { + name: "Kakao piccoma", + }, + { + name: "Crunchyroll", + }, + ], + relation: [ + { + id: 105398, + idMal: 121496, + title: { + romaji: "Na Honjaman Level Up", + english: "Solo Leveling", + native: "나 혼자만 레벨업", + userPreferred: "Na Honjaman Level Up", + }, + coverImage: { + large: + "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx105398-b673Vt5ZSuz3.jpg", + medium: + "https://s4.anilist.co/file/anilistcdn/media/manga/cover/small/bx105398-b673Vt5ZSuz3.jpg", + color: null, + }, + bannerImage: + "https://s4.anilist.co/file/anilistcdn/media/manga/banner/105398-4UrEhdqZukrg.jpg", + genres: ["Action", "Adventure", "Fantasy"], + tags: [ + { + id: 604, + name: "Dungeon", + }, + { + id: 82, + name: "Male Protagonist", + }, + { + id: 111, + name: "War", + }, + { + id: 66, + name: "Super Power", + }, + { + id: 207, + name: "Full Color", + }, + { + id: 29, + name: "Magic", + }, + { + id: 1243, + name: "Necromancy", + }, + { + id: 321, + name: "Urban Fantasy", + }, + { + id: 253, + name: "Gods", + }, + { + id: 109, + name: "Primarily Adult Cast", + }, + { + id: 103, + name: "Politics", + }, + { + id: 93, + name: "Post-Apocalyptic", + }, + { + id: 15, + name: "Demons", + }, + { + id: 85, + name: "Tragedy", + }, + { + id: 308, + name: "Video Games", + }, + { + id: 365, + name: "Memory Manipulation", + }, + { + id: 96, + name: "Time Manipulation", + }, + { + id: 198, + name: "Foreign", + }, + { + id: 1564, + name: "Estranged Family", + }, + { + id: 171, + name: "Bullying", + }, + { + id: 488, + name: "Age Regression", + }, + { + id: 104, + name: "Anti-Hero", + }, + { + id: 322, + name: "Assassins", + }, + { + id: 774, + name: "Chimera", + }, + { + id: 1045, + name: "Heterosexual", + }, + { + id: 516, + name: "Language Barrier", + }, + { + id: 153, + name: "Time Skip", + }, + ], + type: "MANGA", + format: "MANGA", + status: "FINISHED", + episodes: null, + duration: null, + averageScore: 85, + season: null, + }, + { + id: 176496, + idMal: 58567, + title: { + romaji: + "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", + english: "Solo Leveling Season 2 -Arise from the Shadow-", + native: + "俺だけレベルアップな件 Season 2 -Arise from the Shadow-", + userPreferred: + "Ore dake Level Up na Ken: Season 2 - Arise from the Shadow", + }, + coverImage: { + large: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx176496-r6oXxEqdZL0n.jpg", + medium: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx176496-r6oXxEqdZL0n.jpg", + color: "#a1bbe4", + }, + bannerImage: null, + genres: ["Action", "Adventure", "Fantasy"], + tags: [ + { + id: 1243, + name: "Necromancy", + }, + { + id: 604, + name: "Dungeon", + }, + ], + type: "ANIME", + format: "TV", + status: "NOT_YET_RELEASED", + episodes: null, + duration: null, + averageScore: null, + season: null, + }, + ], + }); + } + + return HttpResponse.json({ + code: 200, + message: "success", + id: 135643, + idMal: 49210, + id_provider: { + idGogo: "grimm-kumikyoku-dub", + idGogoDub: "grimm-kumikyoku", + idZoro: "the-grimm-variations-19092", + id9anime: "grimm-kumikyoku.qxvzn", + idPahe: "", + }, + title: { + romaji: "Grimm Kumikyoku", + english: "The Grimm Variations", + native: "グリム組曲", + userPreferred: "Grimm Kumikyoku", + }, + dub: true, + description: + 'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)', + coverImage: { + large: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + medium: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + color: "#fea150", + }, + bannerImage: + "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + genres: ["Fantasy", "Thriller"], + tags: [ + { + id: 400, + name: "Fairy Tale", + }, + { + id: 193, + name: "Episodic", + }, + { + id: 471, + name: "Anthology", + }, + { + id: 227, + name: "Classic Literature", + }, + { + id: 179, + name: "Witch", + }, + { + id: 1219, + name: "Disability", + }, + { + id: 93, + name: "Post-Apocalyptic", + }, + { + id: 94, + name: "Gore", + }, + { + id: 25, + name: "Historical", + }, + { + id: 250, + name: "Rural", + }, + { + id: 394, + name: "Writing", + }, + { + id: 29, + name: "Magic", + }, + { + id: 161, + name: "Bar", + }, + { + id: 1578, + name: "Arranged Marriage", + }, + { + id: 654, + name: "Denpa", + }, + { + id: 217, + name: "Dystopian", + }, + { + id: 598, + name: "Elf", + }, + { + id: 456, + name: "Conspiracy", + }, + { + id: 63, + name: "Space", + }, + { + id: 364, + name: "Augmented Reality", + }, + { + id: 112, + name: "Virtual World", + }, + { + id: 639, + name: "Body Horror", + }, + { + id: 163, + name: "Yandere", + }, + { + id: 154, + name: "Body Swapping", + }, + { + id: 100, + name: "Nudity", + }, + ], + status: "FINISHED", + format: "ONA", + episodes: 6, + year: 2024, + season: "SPRING", + duration: 44, + startIn: { + year: 2024, + month: 4, + day: 17, + }, + endIn: { + year: 2024, + month: 4, + day: 17, + }, + nextair: null, + score: { + averageScore: 66, + decimalScore: 6.6, + }, + popularity: 8486, + siteUrl: "https://anilist.co/anime/135643", + trailer: { + id: "bTU3detmX_I", + site: "youtube", + thumbnail: "https://i.ytimg.com/vi/bTU3detmX_I/hqdefault.jpg", + }, + studios: [ + { + name: "Netflix", + }, + { + name: "Wit Studio", + }, + ], + relation: [ + { + id: 177039, + idMal: 169338, + title: { + romaji: "Grimm Kumikyoku", + english: null, + native: "グリム組曲", + userPreferred: "Grimm Kumikyoku", + }, + coverImage: { + large: + "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx177039-672FYniIpHIL.jpg", + medium: + "https://s4.anilist.co/file/anilistcdn/media/manga/cover/small/bx177039-672FYniIpHIL.jpg", + color: "#865028", + }, + bannerImage: null, + genres: ["Fantasy", "Thriller"], + tags: [ + { + id: 400, + name: "Fairy Tale", + }, + { + id: 94, + name: "Gore", + }, + { + id: 85, + name: "Tragedy", + }, + { + id: 63, + name: "Space", + }, + ], + type: "MANGA", + format: "MANGA", + status: "RELEASING", + episodes: null, + duration: null, + averageScore: null, + season: null, + }, + ], + }); + }, + ); +} diff --git a/src/mocks/anify/title.ts b/src/mocks/anify/title.ts new file mode 100644 index 0000000..47e7cac --- /dev/null +++ b/src/mocks/anify/title.ts @@ -0,0 +1,12 @@ +import { HttpResponse, http } from "msw"; + +export function getAnifyTitle() { + return http.get(`https://api.anify.tv/info`, ({ request }) => { + // Construct a URL instance out of the intercepted request. + const url = new URL(request.url); + const id = url.searchParams.get("id"); + + // TODO: Actually return a response + return HttpResponse.json({ bannerImage: null, countryOfOrigin: "JP" }); + }); +} diff --git a/src/mocks/anilist/title.ts b/src/mocks/anilist/title.ts new file mode 100644 index 0000000..9a9b0e1 --- /dev/null +++ b/src/mocks/anilist/title.ts @@ -0,0 +1,70 @@ +import { HttpResponse, graphql } from "msw"; + +export function getAnilistTitle() { + return graphql.query( + "GetTitle", + ({ variables: { id }, request: { headers } }) => { + console.log( + `Intercepting GetTitle query with ID ${id} and Authorization header ${headers.get("authorization")}`, + ); + + if (id === -1 || id === 50) { + return HttpResponse.json({ + errors: [ + { + message: "Not Found.", + status: 404, + locations: [ + { + line: 2, + column: 2, + }, + ], + }, + ], + data: { + Media: null, + }, + }); + } + + return HttpResponse.json({ + data: { + Media: { + id: 135643, + idMal: 49210, + title: { + english: "The Grimm Variations", + userPreferred: "The Grimm Variations", + }, + description: + 'Once upon a time, brothers Jacob and Wilhelm collected fairy tales from across the land and made them into a book. They also had a much younger sister, the innocent and curious Charlotte, who they loved very much. One day, while the brothers were telling Charlotte a fairy tale like usual, they saw that she had a somewhat melancholy look on her face. She asked them, "Do you suppose they really lived happily ever after?"\n<br><br>\nThe pages of Grimms\' Fairy Tales, written by Jacob and Wilhelm, are now presented from the unique perspective of Charlotte, who sees the stories quite differently from her brothers.\n<br><br>\n(Source: Netflix Anime)', + episodes: 6, + genres: ["Fantasy", "Thriller"], + status: "FINISHED", + bannerImage: + "https://s4.anilist.co/file/anilistcdn/media/anime/banner/135643-cmQZCR3z9dB5.jpg", + averageScore: 66, + coverImage: { + extraLarge: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx135643-2kJt86K9Db9P.jpg", + large: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx135643-2kJt86K9Db9P.jpg", + medium: + "https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx135643-2kJt86K9Db9P.jpg", + }, + countryOfOrigin: "JP", + mediaListEntry: headers.has("authorization") + ? { + id: 402665918, + progress: 1, + status: "CURRENT", + } + : null, + nextAiringEpisode: null, + }, + }, + }); + }, + ); +} diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 0f3710c..0a8cbfb 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1 +1,5 @@ -export const handlers = []; +import { getAmvstrmTitle } from "./amvstrm/title"; +import { getAnifyTitle } from "./anify/title"; +import { getAnilistTitle } from "./anilist/title"; + +export const handlers = [getAnilistTitle(), getAmvstrmTitle(), getAnifyTitle()]; diff --git a/src/types/schema.ts b/src/types/schema.ts index bcb07ff..6b60d11 100644 --- a/src/types/schema.ts +++ b/src/types/schema.ts @@ -8,3 +8,11 @@ export const SuccessResponseSchema = <T extends ZodSchema>(schema?: T) => { return z.object({ success: z.literal(true), result: schema }); }; + +export const ErrorResponseSchema = z.object({ + success: z.literal(false), +}); + +export const AniListIdSchema = z + .number({ coerce: true }) + .openapi({ type: "integer" }); diff --git a/src/types/title/countryCodes.ts b/src/types/title/countryCodes.ts new file mode 100644 index 0000000..db2546f --- /dev/null +++ b/src/types/title/countryCodes.ts @@ -0,0 +1,253 @@ +import { z } from "zod"; + +export const countryCodeSchema = z.enum([ + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BL", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CU", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IR", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KP", + "KR", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MF", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SD", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "SS", + "ST", + "SV", + "SX", + "SY", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW", +]); diff --git a/src/types/title/index.ts b/src/types/title/index.ts new file mode 100644 index 0000000..096b0f6 --- /dev/null +++ b/src/types/title/index.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; + +import { countryCodeSchema } from "./countryCodes"; + +export type Title = z.infer<typeof Title>; +export const Title = z.object({ + nextAiringEpisode: z.nullable( + z.object({ + episode: z.number(), + airingAt: z.number(), + timeUntilAiring: z.number(), + }), + ), + mediaListEntry: z.nullable( + z.object({ + status: z.nullable( + z.enum([ + "CURRENT", + "PLANNING", + "COMPLETED", + "DROPPED", + "PAUSED", + "REPEATING", + ]), + ), + progress: z.number().nullable(), + id: z.number(), + }), + ), + countryOfOrigin: z.optional(countryCodeSchema), + coverImage: z.nullable( + z.object({ + medium: z.nullable(z.string()).optional(), + large: z.nullable(z.string()).optional(), + extraLarge: z.nullable(z.string()).optional(), + }), + ), + averageScore: z.number().nullable(), + bannerImage: z.nullable(z.string()), + status: z.nullable( + z.enum([ + "FINISHED", + "RELEASING", + "NOT_YET_RELEASED", + "CANCELLED", + "HIATUS", + ]), + ), + genres: z.nullable(z.array(z.nullable(z.string()))), + episodes: z.number().nullable(), + description: z.nullable(z.string()), + title: z.nullable( + z.object({ + userPreferred: z.nullable(z.string()), + english: z.nullable(z.string()), + }), + ), + idMal: z.number().nullable(), + id: z.number(), +}); diff --git a/tsconfig.json b/tsconfig.json index 0a09870..6043be9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "types": ["@cloudflare/workers-types"], + "types": ["@cloudflare/workers-types", "@types/bun"], "baseUrl": "./", "paths": { "~/*": ["src/*"] @@ -25,6 +25,15 @@ // Some stricter flags "noUnusedLocals": true, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": true + "noPropertyAccessFromIndexSignature": true, + + // plugins + "plugins": [ + { + "name": "@0no-co/graphqlsp", + "schema": "https://graphql.anilist.co", + "tadaOutputLocation": "./src/types/anilist-graphql.d.ts" + } + ] } }