From 1d606ef0d35872c149c1954fdf234784d2717b1e Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Sun, 8 Sep 2024 02:22:26 -0500 Subject: [PATCH] feat: support sending "new episode" notifications to devices --- bun.lockb | Bin 174133 -> 174581 bytes package.json | 1 + .../episodes/getByAniListId/index.ts | 5 +- .../episodes/getEpisodeUrl/index.ts | 9 +- .../episodes/getEpisodeUrl/responseType.ts | 13 -- src/controllers/new-episode/index.ts | 111 ++++++++++++++++++ src/controllers/upcoming-titles/anilist.ts | 1 - src/index.ts | 6 + src/models/token.ts | 25 +++- src/types/episode/fetch-url-response.ts | 18 +++ .../episode.ts => types/episode/index.ts} | 7 +- 11 files changed, 170 insertions(+), 26 deletions(-) delete mode 100644 src/controllers/episodes/getEpisodeUrl/responseType.ts create mode 100644 src/controllers/new-episode/index.ts create mode 100644 src/types/episode/fetch-url-response.ts rename src/{controllers/episodes/getByAniListId/episode.ts => types/episode/index.ts} (66%) diff --git a/bun.lockb b/bun.lockb index 502ec7e4b57a67f255cad5b6ad60f05e7468adc8..d14c8b01982ddef45631cb7ff510a49deac398ce 100755 GIT binary patch delta 29193 zcmeI5dw34l+W%)BdB`A$Q$$3>IaQH_2oiaSV?>Z@8>&hW62u{KRua{g(6n^QqN=LZ zma2AZx2mnSDoV6zOHU3}R9ojNN*(L>x#v*!es%BneXspqzkfQH@7(MDu6v#DwPwvT zW3ukEvKK!uJ1;U~?)eX&>RkEA3nzSCc7OW%z@V@n=X#^2Y)!3@@r*BX(BFn;uJUMd z6wgl&^EB8te^Q;`hjmS>plJoeN9ScqRMzNGqebS-UmKKP-%rzO5LpfxjJ#M%)2buC zMFt^vA*&+aMOH?>iVQ?XA_I^|N^4pa5YCaLC70qKw!q_p86CSsLkjfJLlqPHdf6Ud6l z?MTsEkfrE_NU62QVMqU%?&iIHu_{U+^Z%=I{$0eWnvX*5Ug*d>5qU2990}Deh6);u-b}(RRj%kdko;vNCdmqrZfd z4CW!F0#lJPyu+OMG$+2Tqkl^S7}0_yt?m47A|*f9;oV7nxD7p!PsA<~NWm4HmY;8C zXCMP6U3C;)2JB0ubp0-*49R9ku0zU@t#;(Kwzhr}83_L(y!i9v(c=eakzcycPX7Y3 znj~}`H-l9rW6;RVk>;w>Vaa8ZG_4M?$J^T;vI{9*w+6`+DwvOyc8(u4BzthSrp0x% zt67O`r8o~#s(Zx@EE5)ZHqoxs9;BpBGTWC4%b(H3wv9)MZFXMMVIxNm(h|Gc)=eFL zC%QDHF1j>*P;SQHOx$WgvTb(&De0Sbv-8UsH903^+~BO-%)HSfCuC}#6uS+ViI*0i zL}EXvAS)wp^u$q`meAc^WJYC-8l62v)5hgB%^96LCaZd?U2th+B@*V07?PbkM$^`! zOC$UBurvJU3gu+x4$H*DQq1=LLHUpNv|XoDFM9=;kE}@Qcv4HQOBogMfHAq5<5&xG zXQtV?R=&q>Zcg4rEHrI$M(!|~Oq+Y#`Am*(Mcp;+ZAbs-i`cAtEnm+sc$k3n+lv@V zKblCXKxW?HjFA~bG6#>$9-mD{EAF?Obqp!?QR#NKSnuHX+xsy*lq%>r3 zR>sH?Cln+8N;eNhGmce7+#`fQjw#`D#-N@*pp-lQk?2b_zK8vj@|Ko zw!YtyZbkPzXjgQHBma(+ad`_VYwar{^9i`u`GpWt)2YbH$l*w-$Vv*Th0Gh5F>ZWb zX5N$NTzCqerZv(N(+1jdO!g4D@@gZq^Ty@I-0CrR2)@s-d&uo^>B)S_Ab04H!Q`b4 z#6Y?(9!;b(Mg`H1UIp)ymb)z*Jt}jYECj3u*@K%79c8tM_}=6rJ=qN@ElxyAi~l*_ zaRa%wi9zP1%)!`bJ%`y{o`jSPS|TOkn2d2*O-E!-zRS_WkW%r9S=rXIk)1~lq(9CY zBEGW`hmrJdx@y=BADx+z$M%vZMG3S>H%6G4| zrSNOXEE!oDIWD(FOR;t{pPXY4Tffn^UFM{5nWN~rS=dPKO~%;6{6D;kwe3(XOry3>A7CbPUSSq;jyH=r=bA>C&zs<5VHVew`Wf}CeCqj(7W5sA)`XRHhfzJrR6C_5rN}pmH z4So7SGnsP_v(O3`H}n~sSy!dTrOd#(ar!kgxsmiQa{X09qS0V5Z*u=_JD}Z07U!a*iZP&7fX~g=Bd+N{z%YxGN-DY8|Pj6!u6Z07JSNhy`${)?-mOi6p zT}|r-XStqHfYu$&Px@5<*fipNdYGBa`608A^9r+=^AD!c%4ghFU(mg>l zP=9^ASsd>(UWK%)BXgX2lHl{iMVNCE67^ALae~kIAj0(%sYB_z+!isRJ&AXjbJ`{v zyC8@DUd^Z`_FO4r2GRvX(4>34*35YWts@#k!-{bpO&a8HxqB2oEuCe}WFrgBwv=LD z|6OC28&@66(lY0bn5J$CqD@4T_IS)dN?3~~dD?3TGqIh|s1@lBu1ud?G_s=(Qrq=t z?a<0uj#RptruooHo4e!V^!}!i%O% zxx7NiwmVD6&aXwB%S|JcYE^}Rogk1NVJ?x~gM{p)FB3{NOOg`&T*`=NMh&F(tmjSQ zylrF_MtI7Rjph7Mct;7n(M4#w9dy3u|RETC{a%b6{MYHy16z3`mYQ-jEPh zEThqJhL;4Qu~s&Y^R!Pi=X6ih=bOddeclEPVT_sDGM+14a;i^%%`8my8OLC|Tgj<_ zr+GWGY>z~}z)bGJ9K-|Y1-WQtw(@UpdoxQ*RGfaVY4r4Y8?gosHUoObd*=|!utKK@ z4X{GpI%rydEA%R%er8F}1ihm7m~#AoqwEOw(w)BR1l*a)Ck(`|be`5|aMEG?}e-HaxAG5cD^ z87CdBlCd&^)))md}ewk;FqR{}U5p!@K znk=98EU%Yr51?0S>&-xGA%5gtMkv87svU2fAS9_-BXV2$lTy0HTJ!WVX7T-U9Wl~< zMtF+7%vh7zI|~g@Xc(_=HH(S$XEd2bQYh6+Mq@;zbDPBJi_OA5KBELqt`gO))wB^i z5wa)XO3e8xw; zH4Qf|sAgG|XFf)u(c1}eMgp4DfXmT?t^84>o%Z%`zoU7r-l%?`rrmGPT|)M$L3CX4hC&ew-p#46^-t%aQW@^27qb;5*qh>YIC`6Om zFsE65x0}WwpHZ!k%cZeWq@mdzCf%pd@I1#s51~nkj2V+8tgr22Wvq@KjMmDElNKyN zyVcSA(O9%(!Z9tXJ@EVL!}hYbSvZ8PA)IuXwPiJoe)bB7-%*GDXfnmENv%I?7H0a4 zOK{R!Yi%;39<((sde|Ryw9=Lq)ZZ?RIx~&#b2NJwVxmbdR-9g9CJ*x&0S~!@$JA$g zTRhC?oekIC8mD4H;%fG;sKNl()no*EpfMe&lW5PPNq^Y=z6VYE5skI_lVs zK)Y&W#wzm#T1zWy>7=q5w&VNDmBJW~CKYAV**MO-0n0jAxXZ$AaAFz3uhGQBy7Cy|L+r7( zwM;b0nr#J3;7e#yaceO$_MzGBmi`XRw0p>|L>gL>WhoQRGz&-hj87a3JLd{R?NaSJ zbVHLGS#jPM(Arp@zn{?Wp6?wrj0M>moxc!r@}~vo(Zr|hTdA0=+x>Gmn#5V-VZ4SW z?I~j|!-3g$?)IwJ;df1XMSlJ3?56YXL`$+tE<4=LogQJvCZS2+Su@i+9gQ`Q<^Ekl z(o@#WpYfBE#%_7T5%wG@FMi66p)tLcy?vfMDVJ~VrIk{UjX z#>&V*2aby~PM|Rz9G~ozV>=b2fr}|LaS5D?m3aqRf?3ik-Ut|V`xx{?>yDMRejBf% zNg8G{3+UHq;+D3L2adK^Ev_=$F=V1iA6V~=!QR?-T2*7gg#euQjp z1)-i+?1kTB+l_N`nMKI9`;3sCU$yZr_Yfg=kyM3%UAyyyQmpLSPH@d0C&bSPlN0=Y zr)o`fa+CZ0>0A=g#re2CG~f7lH|=qqrtK9`uk>azR##J*&b^9qIj_!!GwA zp`I4EnvflPnUI}}Z<=d2n~+^v$?shC=~fR=6#{mG*9fIp*=FegrJa z_<~StI)?kkHgTSS0<+uWiF&44__)tlUck*QQC2?~U!k?Jw6xSXZ>1TUmMs@MoU3q_ z&r|LZbJwgyBjXX<2oG!(XMBn#er(NnZ^%scJ!VN%ygtJ$p6xTz9<}FhIWw(woVO5- zts1f4nuUcvZ_CFtZIq?+6U-c+p*^l?eaMR|U*9-=m{~Z-r@v|z6XTs_dp(1h(aH}c zNs<{)5R&@Z3)zQg?4H}OeiP~l&vF;VR(>{>kcGlKj!-YNi0#leLWx+ghinq(@f4b6 zixQ2Fg>EUk?~h}LU0CGv{s3nc=82kPmVGi&cIHo7zkP8&Y!*KmpI=9_o>X!E@5wUQ zHL|R2DSD)%|1YFRD`zf!D#(2OsT%n@*(5kw{VVB*O*_Z#b}8xGJN)fZ>^nKU$Wovy z5c^~xP3Xmw22Bjvi^(GdZ4eN{Ody6_U93Z-=))YDWwE?CaGUfHW*xVV+oczVHP1Rk zNkuhA6Vl3usb?J`{lQ~E;%5Ol z{)?o1E8%QM;yc!HCn*`t1=6BDqgdVt2nI`yk~IDZ#$Bp6CYtLCP3E;cu5hjdpmE5*(vDE=<58#yT?Bk$H|B zhm=F46g&Yb{WRI(ZJJ{xVXg^Xm?OyOgH92`^E9apLbF?;`rn29m+sBoz6M zWMpnv&CG9C+W62_P0ynw>!MZ0Q5abDfvr>zfEFpPFoaY zdD&M^v`7hl?dT#UxYyA|N^l=1xxgHC_}isa^8~yUd)kp_9lJY7d!dvF8Qfox(vC|= zN%N~CFFX8IAvr`!@S3ClD=GQiz)s{%$NrY2e1iyoiTx)jwuZysE~RDV;Uzx6i5Dq) z1xNo^QtT=^b`>o*xSb$E;sPC6#W55q1y@H(20;!lQhKhIql=VaZAaE|_&Z67%CE~= zGOXtqij;yHB86(qDG2EkgA*ZAE}joM z`v0jE|Np-zKw8@0X@E%4A3_Q>kds^#vXIvNlYkr|B{a4Z zNv2&R{L$zCgl8&x?CF?S?qu0PEB5rksEQVp>51nPnG-grtQz0wm)y${=8{p zE&B7OO%~ceZ`%I6X_L!6$DcQCx8J<6o%!>o?WX*?_2*5SOprfs+Wx$0`~Ps$Hq=_0 zxqJJ+y=i-U^6Fn2wi{pPT-L>@pIv|NgYT;DE0w)G&A;{Cd-^ZCdOhsO(Ms`?jy6Ag zxaR9C_5=^`J5j#L`1hCiZHQQPeo}`aKl+`i?dzB1HP`H^Vdm^f;a#*1d(zFU&jZZR z&r^7tH2d>(v&I(zW(nFvGx&>ib0^xgFH-c$W-;2NFUy;Ef7xB1s`O;NyY4Y-e-&gN z{4#|Xs3X2gH}`%OU>1FqqUW3Y&}M%fV7C1_g(secU#FX`_6C?|&}N#g_okc2(U$H_ z;SIwRXp8m*n5p|x^jYTOed%V`{Q>3;H2z2L{&e#)+M4|-yrX;tZRLRgv;TnZqO@JBjO$u+!_WdT^^gkG2ZbF-H8VA$OchPbVrtsFs2DGeiY2vpjyf~iyEloT` z6Va3zd?;OCWRB&$*evG!tQmedU0-5O=KP$woAXjL;z+vwyg7sOGIJm27tHAI()HzL z;div^D6RS~MSsa`eKei7i05;D**wAd6|?>K={%)f%z34Gj`J!r`G<5~U0lw2wRwf} z>t@=I>G~RT)sM907;QP0qQ7bOJw{uO(-yR~rg5CMpyeD-;T^gSXjvy{%ZU_yotb@t zww$CbXzR`3lMENyw38|P*MwrUNv8tLrl(T$jppQ2>1N~80p>xpO=iSt#tNZ$-`C~JU*OW`lRh&OD_4Dcar)J;twCn;cJDbZtZqmA&Df&e-`)2xr8ak~*`*lIEp1xqG9&MTnJ^ zA^KN_XsT9KhUilTBA^OHGu5{Wgnw0tO+rK~qbkI^LgZA1Xs$L0kyQ;Mv>HUL%B}`c zqdG*15OFHFI>b&Prd5Y%t%`-16a>*U2qIoh4uWWmH>ra{v{ex`AodDTR0G1N_6adN z7@}=3M0-_;$Ej8|ARTJaKMZ1%5NXN?gLqenoG^&qYJ(72 zbs$3PK-{OY>p;{9hbR#uT?L18+9|}eaEQLDScpk=A)3~O=%*&vg=kz4;-C=yRYX0A zy+Rb#gBYOp2{F4qMBDlh8LF^8M5_i6XM`B6S~q|=F2vFX5Si+P5Q`c@q&9>YrWQAZ z=-LS4h7j2*xe>%=A=Wg47@@8Rv9d8l|HcqGYE@&1J`oTB5fGzQ-v~WjAEVZa7^{rC z5VNYPX1KDk2gw zUCj`Yul9*3P|?j0GgP67N7NA!Gga#-#G`7yh{x0k5s$0((TG`UF+z2Xk+w!t@j{gx zBW)F8O$@|bbw!Ak&84l)A&S(h=A?X5=`9dXslFoSskI{JDJT-xezp54|NL$-VTZLGqlG{pKg;>)TVzs&= z#L7fzYa+xNwJH&!j}Ici2l1xr>x1xb2eC@_&zl_`gSf>%Z@+4=Lp>3gpIfJ!CUoSXGd!|Ey0~KemT{!DChmt!kNOrRl9a z`cSpJw?0_Ex;|}~p69WwAHHAzYk>P5gvSzjh$#6_wc_4O*ZsY_+c}(7rZI7J?ug9X zAzJeKGkCN|U#W5)&?kE5@}gY@N$Imv-rbLse@oT0DpIjBy6QDh_xd~khrdXrKB&ig z@4bhNC3>`HeWhgmZoBq(4i88gp=pWgB)%WgaqZu%r_Fr;l5ZP z54I{h@$zzk{Guv97F2cY<|8F2&$IplI4b=9E0a8$$x&(B^eTy?oU)#A?B#L#Xop+i zaPoee`&@LP!&QXyden>C^oIFANhr1h2UTcsFeBp3S3Ckl-$wXe&;GY&Hwda`qsrv0hy`ieJLw`75y6|aWf`KH+ z05UlSgCQUj$Wvc=vMj$5)dvlLymn9t$O|0%!2uu>=pgtO90G^I5%3+5S7c-Yh{KD6 zE3imj4HAzQ4_*qM2g|?y< z$S)|_U;vQm+)jCm_1^gj1lxi{Ag@`}0KuRp2m$hnMrDwp+b@>rggu}X@B>~@8k7M? zvH2eS0Ay7-24p389LxfH!Pj6b5dYt(vp~H^;9amm9VynUw3pfc8dweFXYpxZI>-kF zU;>y3CV@;a3}k_9FdU3fvAgto{Zk3{06jqx=m6S-a8MWQqPHp_%YqYxPl8k6G&lo( z0%yVZ;0N#{kQXrJEz`fNH53u}9>H!P1#o53BGr*ydXP7kU@LWFmmZQYo-PYgK9~!h z0J0d#n*|*}dr$||0`lrhRZs!QdpKo5IUtLYyg2kTmc$3Uj@C*U3MHz4nI ztpPK{fgd68D40b>CL>3KQD7i=2=oOHfPUaX&>!3bdV@AVUg5k6G{QQ#LGU;@2DX3? zz*bNQ=74FS0L%ovg9OkP_=D}le+Zrg^C;hXkNI)rBycZC2YtZ&%(Sx5 zixYkuDJJW|X7G0)#mNHK9DEJF0H1?BU^n;_6oZjq3y{9w33h-Fz;>_|Yy+|~eh5C1 zekvjGG584V0-u2|fh-4KfqkGAXbO&l@4-=U1RMh2f^UGd{UA6Dz5^2X1Nadf17auf zr-7{GC&3vR>YoUl2j{>qpfk7tI)R(u3XlRXgNxu2kc@r>lKFLT6nOLNEtB37!B&KxWzsUA^|Tet-U}=&OMAem3}!a8Kl4 zkZZu}U^S59#c$sPZ-95fHn0`E1J;4R0!jN8QmVEV`8IIV2rpRgNb(r)dxF0~ZUAEZ z9@yyU?;|&ZP2lff3-|y?Mr|l04s0hZl@+-IMD6Fqak7^BZm)jVdjEZTLr)MhrojDr zlWa!qqmycKKo1U)ajJ+x)2Ca0oH!!u8IS%@Ok`YS6r-aC9MGHd!<~6RPvtkYBO*?! zX5Z++vF>MOe_8d&*LB+I2C1SVTSrB<(A*EtHgCIN@rtuMzayr3WQ^n&siu9S4-0v< zlyyH?@Nv-6&1;_@8}Fx+EH*Nlu2nZBZ}&^NEiU#N5dQSKk8T;b-_p(5abKT8Z+@>^ zF&9Umo?w-mubTNE1sth{;t| zzGFNJRK|CtDN<7rA?{~{2MntNP`3Dsj!AT}{4xDR=jXw;m)-v^`2rd#r{u zcdKX>of_hPvv^?g;Bs{f@7r&cL^`Jzf=abu%e9nBhPYojo_ucI-UGh(rehgJ+hba2 zU#Q*E3agl)Qc+rx^8P?ye5rbiSgMx)Ky0|$hzN1NQvBP+_gjZGss5o=UJEJjJ#|(L z+|L|`dA@rn?Wdo_+35Pn=Ikfb#+zhxP__LLhv}roB0}7+A@|+xlOu-dpENY?*$W6DF@rVsY$Zp>K~aEu}fRQqETSfctNYW;CWm1?Sa z$MhN@?gx~I-~X2tM=Q;|V)c;Kwk>MwG2QiU9t#B?r|PL8)@7}rQ>U&^JY3Z;tE!&Z zmYU;%oz#HiR6PTO8jNUiMeXTc4cCr!445>Ni3uhqqT|+ff!kMJk(d~38GKr;A!*18 z460#JFyO(b%DnVh12Mp}Ik?frK>tKt6T7dmWBL@-tkxiKM!nu2TXwP^(~hcUC-mT! zKVyJn6->DIZf}E69(&TRLn|4uTJ}P_>Em^s{TFxcS5?1D(s5L*rKX>tKSHrENIUiP zu#(x8Pu0C;(NL{Dp`YLh+LDu0!ul>VcFmfs%}hNz{V}WGT1L{KC)KAI@MHg$Gdw!< zoYGr_946Cp7-wC1xWLc1u8T*P@s`G)Qfa3!zN|(dLfo%U7j?QhcJ9-^oROuod1N$g ztF4w{5bJ)fy4i%o^Ng>;*W5C2KWM$MW!_ucYkm0kt(XDojO6Wp0ekhfB_pmZX+7$e z0gp3IQ-`-yya>DA{OUd$EjA#Wy%IH#+cNi^g&mH3N;7c|>B%r+ST75Fubj9ZQ+w1! zVyG$?A1y*{B5BAU7|`l1>YS9cN0mE+$5vN9I*JEx?SH{z-S1!#6MBO+`g@0B-KU4j`s+6B;t^3LAKKo<)cfap)9a6C_OGnLC`55p(>FjyT zUr;5IYPHhOS!CFGIw1O-9vAC=R6F^hVaI2_NyShsqMy-hva_vbfzps3-x zw!8)Ke5CC;D^&m^9^!uEdsV|_-HN|o_p0ajIcd9u;~8q)FZv@P?qiz6+qD^~i(Ak0 z=nI?K!?s!lUZh9YsV0aJ_w&~gs>1R2FV(2&3`Z3+O*XPI8A^YNSyWCljZMMk&Q($u*so~m`Mu>0lk8dtAI z-?OV_<}ItKYV9RzK2Lpw2ywqW9uU;=#Pe@V*m28lgPIZOsoKWM&HY|^_J)L&ElTJ7 zc+1NDqWOWDLpw(=+`r{k%muZjs;6qamEQgkI-d{koZUay*XX%hR_>?LcQ3CLx~=hv zp0{F}$EfFiWukUrs#m9DMh<-O=2!DKw7+H1U!_%cYv_KG{n)~B=a>5~ZMFRWKg?fQC8*1Az07Ww%5v!yvYxf<2BOAdJRt{pL*b`-q926Q!iZAXY<@N;+nO{^uI=NcUZjTx`V&2 zQ}Z!kimpcF&u?#C7z?xx*^6t&&S>nX2m9<6uOTLsmVfeH{D7Ic+mGIg*+ookV%8>l z(vByWdf`^gabm)Vd3Sfs?t^FK4!9LlCP~xk5Yx7}!Pgy@%^&QiKXb+u_1)%}JZ7^-cU9-6UL)51y!$_PzbZhpdvV#V$pi#oSZ3%Yw38 zCi^XD>uGNFS+Krf!CdaN?3U|~F4*iz^~Cp36?9LH5ch-iv&uv~pZd~UEZl6~qvZx^ zzG|;~f~&Y+s&CL@ebJ}=%UAdC7D#P^pq zn!e#Qc2SYs39^mttp<8L-97bst2aEJ8nxWd)i+%A&DHy>jGbfUXx%g?^;Tm_d#d^2 zSoUQ@@2=uYQ^sD^tdwU4_iJlQp}nX+67jbBp_He2tos@Mw!=Of^6;$T^~o+qx+hw5 zzy5xD$OHEsOnAQ|G3-sHO0!g|A31DQ2g)G!sTrcZt(M$Iu27$fc1%_Drx)B0%ikEO z|7A?wj&dg?_t0duRfT#naKF(%Xl(tq?aEc;1qk<6BVVOqKt>}Fp1{89ajz#P*8RkO z-Q?tEqd(pKtYvR?g8LEvFUGeRI_>>8&k!SKi?#F-I^)+JMPy4@iUqQXsx-^sRb}RcA4fo^!FLe8=Tlmt1pRCN;iSnzG+QcA* z{IS7Qbq<5rf8L>Z^r8pth1dOi07D<2e=_9art#!0(~CC0qI_jM%|qP3B(UPX6|XJ+ z=mBoW=-$?{0{vYTF@_=TpB`xP)Dmq?-342%+F3d5QCnn??XeDV|75{^pP#NjWpt&! z*l{xN4T4%dH$Xry#IB`-;>H4Tulw;b3c_KVwzfjh)v9}SNPN88c%6G zw}JdZ;p|qr5hHg%#?AT5297(TTWw;Mh~gr3!=Q+PDyS?OtxzdtX`%Z?^B&DUcrkd_ z)~Zg;>HoJ?J_fWA(kK>xNHv(!@gvB)mPQ)P(SQ;s!mkMfph0Pm2!+)I$XQ~P5or#z`=sacqN z$`4cTm1nJR|C~YNbKjoL3;KCIwUQ}Jqr--&pRpi+8Qxm%-%I$&|3t%XJ$8NUbSC+? z9HtTjXkgMXd)x8y!skzW>kMpuR1 z(^C?!^zxJ6rmXUR|C0%$f|?~m@Hn+7aGi-O<+*%ga<~Ep@DMwVMCU@M#T6WGR)(YQo6Klm-n^>n4 zK2v8T%RBC5?QB$d6^3Vvibr??CaL?Yu%^3z1Y%*Og7(e(ztM@&jf&)^I$Cr8EX1QX z*FAr&Mm5>5h#TRZ)&|k~S?`Xc$l4oak76+Ip_Z#kq0VSoG4qrU>dvEO8Q8ZdrLAy@ zDiY&64w+?O4Oy_Kbd36@Ds%6SV`CXIHkGRV(b!lP_Kk;K?pW2o+8+&&Wo!*lu>P7_ zjh*fcgcb9dIs_Ga$APd6`V`ocxosS34D&Z8vQ>4J zfdMmfs@S=I_pD-v&Ic;CJtNoeIQl*^Rx4C*iov28_Bvj@%8%U}&+fX#qjTS8JvvyS zj#g)x-l&3tJWY&`v6EGOiAo6~*``PAyP@_~S7+CJ^Zu!~X8abFk3sBY>?>h^YWUR0 zjW#Wp*#*n>-u+V*zdl#-$m8$s;{AWA5;rDM7gUKPb^mxqn+w86^8&23?@$1lYF10G2|@9UXbWd7YPIXc!Z(km^qnqG}yEcM~nRQYkpiVg~m z(%e7wv3YEx1E=%5$#u@EPK> zQDqTHM69;zxqSEd@uYH#M%d#J24d>YdzZW_!vwz;^Zs|fzhS(yjs`wC37{7mT zXIE8?4)J))whFZ$G+ho=(?dK-Jc1fFc4XwZj3F7@Crw)YM8b>j^i{hhQD1c~#N*RH zSJAaRVQo4&zlnIRPJgH7N0mOj7hPN@|B_Mav58;j{BwL`c@>7E|El$KYI*wVs(F&9 m^7`*WJ^W%fJjqjAjeW`!u1>Z0bWzb0J+&lctS925%>M@=7Tn+f delta 28885 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 diff --git a/package.json b/package.json index e9683e5..027d669 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@haverstack/axios-fetch-adapter": "^0.12.0", "@hono/swagger-ui": "^0.2.2", "@hono/zod-openapi": "^0.12.0", + "@hono/zod-validator": "^0.2.2", "@libsql/client": "^0.6.2", "@upstash/qstash": "^2.7.0", "drizzle-orm": "^0.31.2", diff --git a/src/controllers/episodes/getByAniListId/index.ts b/src/controllers/episodes/getByAniListId/index.ts index bdb2259..0bfc8d7 100644 --- a/src/controllers/episodes/getByAniListId/index.ts +++ b/src/controllers/episodes/getByAniListId/index.ts @@ -3,17 +3,14 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; import { readEnvVariable } from "~/libs/readEnvVariable"; import type { Env } from "~/types/env"; +import { EpisodesResponseSchema } from "~/types/episode"; import { AniListIdQuerySchema, ErrorResponse, ErrorResponseSchema, - SuccessResponseSchema, } from "~/types/schema"; import { getEpisodesFromAnify } from "./anify"; -import { EpisodesResponse } from "./episode"; - -const EpisodesResponseSchema = SuccessResponseSchema(EpisodesResponse); const route = createRoute({ tags: ["aniplay", "episodes"], diff --git a/src/controllers/episodes/getEpisodeUrl/index.ts b/src/controllers/episodes/getEpisodeUrl/index.ts index 5b5de66..51710f2 100644 --- a/src/controllers/episodes/getEpisodeUrl/index.ts +++ b/src/controllers/episodes/getEpisodeUrl/index.ts @@ -2,19 +2,18 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { readEnvVariable } from "~/libs/readEnvVariable"; import type { Env } from "~/types/env"; +import { + FetchUrlResponse, + FetchUrlResponseSchema, +} from "~/types/episode/fetch-url-response"; import { AniListIdQuerySchema, ErrorResponse, ErrorResponseSchema, - SuccessResponseSchema, } from "~/types/schema"; -import { FetchUrlResponse as FetchUrlResponseSchema } from "./responseType"; - const FetchUrlRequest = z.object({ id: z.string(), provider: z.string() }); -const FetchUrlResponse = SuccessResponseSchema(FetchUrlResponseSchema); - const route = createRoute({ tags: ["aniplay", "episodes"], summary: "Fetch stream URL for an episode", diff --git a/src/controllers/episodes/getEpisodeUrl/responseType.ts b/src/controllers/episodes/getEpisodeUrl/responseType.ts deleted file mode 100644 index 78d046d..0000000 --- a/src/controllers/episodes/getEpisodeUrl/responseType.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -import { SkippableSchema } from "~/types/schema"; - -export type FetchUrlResponse = z.infer; -export const FetchUrlResponse = z.object({ - source: z.string(), - subtitles: z.array(z.object({ url: z.string(), lang: z.string() })), - audio: z.array(z.object({ url: z.string(), lang: z.string() })), - intro: SkippableSchema, - outro: SkippableSchema, - headers: z.record(z.string()).optional(), -}); diff --git a/src/controllers/new-episode/index.ts b/src/controllers/new-episode/index.ts new file mode 100644 index 0000000..dec04d6 --- /dev/null +++ b/src/controllers/new-episode/index.ts @@ -0,0 +1,111 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { env } from "hono/adapter"; +import mapKeys from "lodash.mapkeys"; +import { DateTime } from "luxon"; +import { z } from "zod"; + +import { Case, changeStringCase } from "~/libs/changeStringCase"; +import type { AdminSdkCredentials } from "~/libs/fcm/getGoogleAuthToken"; +import { sendFcmMessage } from "~/libs/fcm/sendFcmMessage"; +import { verifyQstashHeader } from "~/libs/qstash/verifyQstashHeader"; +import { readEnvVariable } from "~/libs/readEnvVariable"; +import { getTokensSubscribedToTitle } from "~/models/token"; +import type { Env } from "~/types/env"; +import type { EpisodesResponseSchema } from "~/types/episode"; +import type { FetchUrlResponse } from "~/types/episode/fetch-url-response"; +import { + AniListIdSchema, + EpisodeNumberSchema, + ErrorResponse, + SuccessResponse, +} from "~/types/schema"; + +const app = new Hono(); + +app.post( + "/", + zValidator( + "json", + z.object({ + aniListId: AniListIdSchema, + episodeNumber: EpisodeNumberSchema, + }), + ), + async (c) => { + const { aniListId, episodeNumber } = await c.req.json<{ + aniListId: number; + episodeNumber: number; + }>(); + + if (!(await verifyQstashHeader(env(c, "workerd"), c.req))) { + return c.json(ErrorResponse, { status: 401 }); + } + + const domain = c.req.url.replace(c.req.path, ""); + + console.log(`${domain}/episodes/${aniListId}`); + const { success, result: fetchEpisodesResult } = await fetch( + `${domain}/episodes/${aniListId}`, + ).then((res) => res.json()); + if (!success) { + return c.json(ErrorResponse, { status: 500 }); + } + + const { episodes, providerId } = fetchEpisodesResult; + const episode = episodes.find( + (episode) => episode.number === episodeNumber, + ); + if (!episode) { + return c.json(ErrorResponse, { status: 404 }); + } + + const { success: fetchUrlSuccess, result: fetchUrlResult } = await fetch( + `${domain}/episodes/${aniListId}/url`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: episode.id, + provider: providerId, + }), + }, + ).then((res) => res.json()); + if (!fetchUrlSuccess) { + return c.json(ErrorResponse, { status: 500 }); + } + + const tokens = await getTokensSubscribedToTitle( + env(c, "workerd"), + aniListId, + ); + + await Promise.all( + tokens.map(async (token) => { + return sendFcmMessage( + mapKeys( + readEnvVariable(c.env, "ADMIN_SDK_JSON"), + (_, key) => changeStringCase(key, Case.snake_case, Case.camelCase), + ) as unknown as AdminSdkCredentials, + { + token, + data: { + type: "new_episode", + episodes: JSON.stringify(episodes), + episodeStreamInfo: JSON.stringify(fetchUrlResult), + aniListId: aniListId.toString(), + episodeNumber: episodeNumber.toString(), + }, + android: { priority: "high" }, + }, + ); + }), + ); + + return c.json(SuccessResponse, 200); + }, +); + +export default app; diff --git a/src/controllers/upcoming-titles/anilist.ts b/src/controllers/upcoming-titles/anilist.ts index d03a2ec..3ab1daa 100644 --- a/src/controllers/upcoming-titles/anilist.ts +++ b/src/controllers/upcoming-titles/anilist.ts @@ -52,7 +52,6 @@ export async function getUpcomingTitlesFromAnilist(env: Env) { env, "schedule_last_checked_at", ).then((value) => (value ? Number(value) : DateTime.now().toUnixInteger())); - console.log(lastCheckedScheduleAt); const twoDaysFromNow = DateTime.now().plus({ days: 2 }).toUnixInteger(); let currentPage = 1; diff --git a/src/index.ts b/src/index.ts index 9930c7b..7eafd0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,12 @@ app.route( (controller) => controller.default, ), ); +app.route( + "/new-episode", + await import("~/controllers/new-episode").then( + (controller) => controller.default, + ), +); // The OpenAPI documentation will be available at /doc app.doc("/openapi.json", { diff --git a/src/models/token.ts b/src/models/token.ts index cb8eaa3..c220720 100644 --- a/src/models/token.ts +++ b/src/models/token.ts @@ -1,10 +1,10 @@ -import { eq, or, sql } from "drizzle-orm"; +import { and, eq, gt, or, sql } from "drizzle-orm"; import { TokenAlreadyExistsError } from "~/libs/errors/TokenAlreadyExists"; import type { Env } from "~/types/env"; import { getDb } from "./db"; -import { deviceTokensTable } from "./schema"; +import { deviceTokensTable, watchStatusTable } from "./schema"; export function saveToken( env: Env, @@ -74,3 +74,24 @@ export function updateDeviceLastConnectedAt(env: Env, deviceId: string) { .where(eq(deviceTokensTable.deviceId, deviceId)) .run(); } + +export function getTokensSubscribedToTitle(env: Env, titleId: number) { + return getDb(env) + .select({ token: deviceTokensTable.token }) + .from(deviceTokensTable) + .fullJoin( + watchStatusTable, + eq(deviceTokensTable.deviceId, watchStatusTable.deviceId), + ) + .where( + and( + eq(watchStatusTable.titleId, titleId), + gt(deviceTokensTable.lastConnectedAt, sql`date('now', '-1 month')`), + ), + ) + .then((tokens) => + tokens + .map(({ token }) => token) + .filter((token): token is string => !!token), + ); +} diff --git a/src/types/episode/fetch-url-response.ts b/src/types/episode/fetch-url-response.ts new file mode 100644 index 0000000..b15d336 --- /dev/null +++ b/src/types/episode/fetch-url-response.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { SkippableSchema, SuccessResponseSchema } from "~/types/schema"; + +export type FetchUrlResponseSchema = z.infer; +export const FetchUrlResponseSchema = z.object({ + source: z.string(), + subtitles: z.array(z.object({ url: z.string(), lang: z.string() })), + audio: z.array(z.object({ url: z.string(), lang: z.string() })), + intro: SkippableSchema, + outro: SkippableSchema, + headers: z.record(z.string()).optional(), +}); + +export type FetchUrlResponse = z.infer & { + result: FetchUrlResponseSchema; +}; +export const FetchUrlResponse = SuccessResponseSchema(FetchUrlResponseSchema); diff --git a/src/controllers/episodes/getByAniListId/episode.ts b/src/types/episode/index.ts similarity index 66% rename from src/controllers/episodes/getByAniListId/episode.ts rename to src/types/episode/index.ts index b85332c..7c1c6c2 100644 --- a/src/controllers/episodes/getByAniListId/episode.ts +++ b/src/types/episode/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { EpisodeNumberSchema } from "~/types/schema"; +import { EpisodeNumberSchema, SuccessResponseSchema } from "~/types/schema"; export type Episode = z.infer; export const Episode = z.object({ @@ -18,3 +18,8 @@ export const EpisodesResponse = z.object({ providerId: z.string(), episodes: z.array(Episode), }); + +export type EpisodesResponseSchema = z.infer & { + result: EpisodesResponse; +}; +export const EpisodesResponseSchema = SuccessResponseSchema(EpisodesResponse);