;IGdzOD!_IsFCPxA?lJ&pC201(mko334Ax#U+MB@ zEkDLn_8yR+TZ!uY-8f!t9jalq*wjDp7QpW*Pa#sA|Ft@jxDb@R({vWdouZHMP!?rR z;S4tsT4Esc6)zD{M#!Xp|;Lbq?dc=6oHl z9<`rb?Rd@p^7J%-<$YF2AQ~&>TkpG7(lbDQu=Xc(?Zf5wJAPq&I`4f1$^KsE;Z&>w zJa`E$3{4*8S>`}#5tDgL?5v8f1DOKc{&GMqi D!~fh+|2Akq zk%at%^;lsZXw*)Lu88mSd|3619TLkQE?L~cy12L)Uu~3aaM{rUtUotP`VqKO$BL4j z36_eF)kNdjbm2;}R4>rZ2%4l4CZi&WE`5k3{Pp#N6R^OBy7qkJWQuN0-)@tTI6NBU z5JOi|VrIiZ>uY+kJh~=%(O?zIgJ*yU+S5%}3!Y4Vd%5VQB1ta&5EXjA+M5bc9@{wB zBZ~;yrRr>WdXbCk1L`%=(w8*@J2n0EHF%25kjN%KAP56v{m|GZOz_Rm#(x{qw?Lqx zCR4QZ69B7}{}Nm4J>TeJ;KkZk%3>#wx{yyO%8>W%ql9>^Z9i#y-vcoIKczv*{wA6# zc~VHGB8+B2@q(ts%$11LN#evQ*Z1lFJ3)tP (=9$!9b#pgJXz- >ukgGiv1O5I$=v}XX 0I#DiS~-L7CaPVWC@f51jWIK^?{fe)lb6@x|dm?6w{<4 zXh8_(&S?V2T~v|v$dVGpjxZ(eXO0&WDh$L^%Z( 9Y^ipM7=dIT}}A2}KzTw+CKuX-ks2VL&9 zKw0tJLJ%^ZUwxm6W}XTqeGIsIxrwnL3i=YFi3odWb&xjGxyO{?@d6wX>}~(rPI-Q` zW8%C1_U%9S!OL}rU c=)EA(k?G~I4H@CsfSyIVp^COSwNIX9HLrE# zuTxDClzCD{((Rv8bF|c7Ke#)pD;lE1O9x(s%y)%-TmYyA@}K*I<5JMthhj)*9eWWV zARw^4YF|Roc*}8{H&@Pzl^|8twGaoL+L(d2XIuFIQH$r-b*giy$H7u`40NdEX3Xhz zX2D}KA!q8`>}=-wCbx{ClK19Z(e9tG87ujBVDGxd6cR^AN4 !@yh2V2&t)Obnw }0HSFf*Ci7SFM~slW=}@2eclN9O{rxGt zj4__TAul&t00f7Gv`6JBKYr{0B_g{2KtN#07cV<}J7-?&>%#%bK1-c1&L;E ZJ14c>D<@4xVM 8K0x|ewLU$X)X8JKN!&h9UQ*F9Mk#d(b3TJje$`GwzsoR!~=5s3kwT0Sy}_~ zs4|*HMNcNw>-;#D=$VVGxVX4mY8IBumLJ0zAG$&>{!oB>BkWwwzeFl+ZVsi$GcYpV zN-9o8Rl6}Oro0&hko=mOn*F5Fz4*ponJal7FWp2x#WGr&5 ) As+=+cRn@b18aRWjn~ zVs6z$3+d2v;VA`Op92{gnFn|xdxLJOs@d #6MClJ2K=zxOn#Pqyo|{Dj z>W2JjXs@6UPM?4Ph8ICWcdxFmJ0}t-`q3fe?X{#)rR)_iZ)379$$A?J-CPceN=pxv zd(WQDct0@M5ip}G_$Y!3=NK*_9CkRyx11kTyk`-D$FOXD!5YI#;^;(n*~Ny+O072o z=-I9)#Z>{DZgWnaDYzXceY+S(qp}{8VUl9tLlPZ1sMtaj-0XFiANY5KE$;`_l~{Oq za`}uucX|)mR&K6>b8a8gFA>4F{U&loWzo(a`UCNB+Be?8T=t?@T7ntFgz2@&uqF*n z7=jTp1QpWbapgBFI5H8-c{y9gtdN}qH;>A4Rn+_|?08BYIy$ZKCfz~mx9GIvv`WYA zaV;zV)Z`?u-NNSjPS7;$YK*ojiFNo%yhydf(Dt1&sNSTg5D6)%z748MMSw1fIUzf~ zPLH8CH7Sc d#3{Vt@Sel8y6h}%+OS6ObsI{tU&S8871BH}I zWn3cnDU?q750G^hspn`JK0dy;BCB=m5@G$ywso0)zG{j}O7xi`^hC4!#L3e9{9njs zSmci#T*Dsgh8`-MD$e9J9icE58yg$+Oi=ZsPO;hMD4Zy(A7>U!tyrJhQ6pYepFS0# z?9-@0O2B~6YTH>6IgAG`w>G9xtiSvFH-GQEQLJiyljYNhn@UEWF;N>`l}g$v1w(R? yvQ_t|wI8;vg-d!OUC5Qi&R!{HVQdz4B4bGidx#Oi@K>e|{a<$^Km;VP@^=a<_ literal 0 HcmV?d00001 diff --git a/src/en/hentainexus/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/hentainexus/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3d95cadfae380714b5cb3b73ed2602875a48b80b GIT binary patch literal 9007 zcma)CWmr^Ex8B3hIY (mXp?Y zo!U;t^Q0U*`td8J{NtPw7J;%7+2@!?UzG7P1KH$T6v#1|QfmvcHp3KQAsI><8X7Hl z!`Xb&LmVgAX&IawT5{K#+VTCT*E?IP#xjGYzh<{?uX;+X^2R+We{1!}b60qEj@M;l z?3u;?>&ma?Oz3gHn-G>zsY!LDQ5cyXm~+>eO*>Ov=&Cy;4#j_=5J@+-IaOJhWm4xH z*4io~e}1%0b$WU_Jn*#F&_%m(6$b|=)_HUM2{|2|d)y^{$$}$Gmx9BT h<+Zir zZ*UZF-@B(v!XTtR*5u ~(b$wVpoBQqk0mmAHMolZT&w z44&>$Wj!P_oXBHTKK>?Q_uFKZ%}6JOz#{3#LD_euIC*mo9=7gBTlvarYI=*ugmNT= z!T8y~CwWk)QYKXeS_X!%0hxv2VPT&h^75Lb@Z0|JrFip$fC#kjw$G2LC@Do(b$54v zeJ3~6J7GKgSy|iyUK;LBOG|rbj%-~xOcixG!on|B?+YNH7;SJr)U;?46T30je6Nkk zAS*jN!KBtnW4XQJ&Ye5=tn1f)H?+33RjTi{_~TjiLwj|N=4)B;nAU$^S (-md!(~wGl>g-LmsR?UB3Q!iIq!fNFH-a2v(AX2xZ6)Qd=Z@5z;C7*XR(~a+>v|^ zESA$*e0#{o)_TPf9XUTgZ$^$p<^}W)qaBf3n=omwi!Fas3HO=rud`q87e+CPRvmXn z(j)Jg)XH&l%iJ@qS8BREnihQV;)TOK{Zey*H-65gj6( pOM3x-PqO zChP0#`K9#uD*+Qfekj6_xwti&B~d4Ryc`@HG?657%N|W<8-?`6#x-v$TiwiRye~y; z8TBSxeuqhU9zlmmOiU#0@9QJ6o2#!;OqcNTB-atLC|n=SkDD4BJ521#R%lnsWV?A& z4o7x^?bldl^)W8UB%$N89xgViTZG*2+*qQlH|c(OT%&Uv%gkX}bSXSr+_=ekdrGHE zeouI7rFFBUNrD-7{`0%dai+hZmHs=6@_4H(XLeRr&q066#^tRGCy~^L0s^{R zG!6i c7AO7v?FXK;oB}q^=;Lbp=_=gJs8S9|S9nQ%y;#zNXPsXx z9AYo?$2L;n&24S6p`Z2mI;l2DNQ%swym8CV_kziouUcs4O%{!Mo(5Tczb)oB3EAJq zMnOTr@yB~cdZ+EFF^aTiVP0OXz`;688P-#@udnajoAv#fnVC#obN }b=OG`^L3piK^+*@mhXR5M6#BR%wllidBAtP5ygL7&TO62Xe?=}V6h}x+rDFa$g zyh?Y6i;M(Gwp1-flpnH#N0O3~cQC$t9;~bc9aa2F3^12>qcq2FcLy}>YhJ})U+((k zDMr%ie2R;s@cYE_T{f6>XYyqX0~fiklBEsBeRHvp8==L?lf!K%mx}~95a-8o#z$NI zsiJD3Y0N8cb_br8sVmv(m6%?N$?T|OE+S!QzGq@@|2%$|bkD?&SJ3?PW7smXEa~m@ z=bw?r>av)tz8C} YLSosX%P6%lnR}*)myLP-fc34Ukw@@_Po8l7c>9N`?7F zOs4~+JO3*BZW&QfP-M&Q?glfkV4ZKz&dhXDB9Y9Ly!B5XC&A3iD)1&NZ6`dG{KEJ5 z_iq4c*#~S+yA8)8nH0J@+zr*1C)s~7n9+cMfVH;PRxI<2vcWnhk=b~gkz7Urf#jm} zjwRrEvG;?ydBFpY9-Wz)K}jv(kS+Kjj3482+ToywXJ<|P+uIn0rBD$~cks6gCpNA6 zk{>K{!keG+08%Eg6ISpYS>|)QHN7)1Fwk4c&t(7LV9eXwdt5Ex1sI$bZGpS8;^D)< zox$@M_)mG6eOl ^R12z_<+jLng}C=piHmLtGIh94dh(j`NR>YASRXxaMpEozLK z%$%&!qcX?4w04yh{&z@`nQ?2nx&*KwgP5aGoQUqyxj!HTp#;zfPGVMd>Y1o9SoHm+ zf$L*luTme$?Wvk@=iM3QI+yKQ`Ho%ZMVHt{BYvvuYL@|~yVDv9W?rW{ZVuMnRq3K0 zi4tOt&7V_69fri~oJWp|rq|j-DX8sbb$%Z#r;oh1pC+03%NS>?t!#9?;lA^$;bTvg zLtY$*T4H)|4NEd`#0O%-P{4?}qOq;bO-)Vv0D^{Q&3{gkfxuA!xOPneA zam6E$YIg`i<0+G*lnI`2-np`T5?<896C1zbMwEuk|g0dtQB6;!?M&g!5H zTh0qP(s`5mYRr}5^5DQSw40LdhD=<#t6x9X#lh-;9z6(<3f`$%O{*nnPUsE73;y#n zMcx|jR=c5`_qvvSOfM_9$;HJvp|S2=cw)s_2XehDhW)u_$}5Tb&fUqz7hL?LHga?f zZGGK2!vZN)7GniEPM9@~@QOu;l9DoigQ^t+&E=CF>vYpi !!VubtqHf^0G ze+$e~w)kjD3`obvD WHK9haPIB#PoN6z^eBwc*ipfQ#X8cw@)VT-N}d= zEl>d{AJfp$Zy`imMK0A}&K5dD(}Zt73=4{)QQc*IUUm~qBc^`{ypMm+8nIF`g$Ndi zpn1^x)c(t(v^R{^PvXlX7$p20vu}Z)4T(z^0g6frN`mh;w9f{7uG(0mvjKvh>PZKC zI1g#_ktnRW)&5JEapQAhUMsrRpJx({)G+fYj14h58-~hq4WJThrVb-15dn#IvN{Lg zb;N4oy<36-HjS^CR^UYfV1x$XN`X>0BKcrNZ36Tp9gymW%Pq}T$#cEYEa)C+3x}Bl zZg{h lKd}HoKoRl7tm@MJHT-)8L$5P zOa4< N`}&vpT6td-lh`!5;w?>VanZ0ZOh6zF?+dYC=yE50 zkZfNU99m^T7YGRB0yZ+l4?}&Cba8Y8aIAR`LumPf3xnFSCofA(mp!MC9s*+Y7@#qg z1zl~KS$+pG!MJgc;^ke>P~Zqit=GG~qXtG{o()#w{B2*$@&E0nd&7 ^u$}Ieg{&eBNs5LQjdVc%n4fp9z4t0G#wK|!yKOj zwrm~|qRlSQWihz*;Tv%%$^+8zJs3;1*K(79dUmt
hm< ~e%+5<>M=aBBC7_pl5nHpRd+1?DjEaYSV z35i*Wi?;1NPTiet2##H*=+Y^=oQ4E32QNv0VDWr(|Mi5KPfiOQ?@_k2kIN-3z-@j7 z5WS41wXQ>KZq1_{9C~-i%mEGh5tzgRc7nurt55ToeLgd*rRQ#*7 KEkyv`G)xATt~H9z?aseo@rWJHw2q ztwRv3c--x0+JgfJ3Jb^=O1S_onQMsQ^A{tzeIIY)uS9o#lGCrx?HOn@a#tDH?#cdl zykbBjfq6i +rwL{V)`W?*7%C4x(4#VvH?%Z^uA}`RzYTlbCQ|ARCf{h`Ye~OMXs)~$8dha(q zmc01u|DofxUhgXPMrd`Hb}VEF>O`)02($%|ikQ^D0mzWz{3^;hZnth>pu#+{B_?%| z{v8=~3Yc-5E${c-yHmtlD5DPt4_+|`3Wyy|qNb`Hm`wzp&1$%jS{xUf#M69~66q`Y zJ@miM?Q-(Clpi3n1)gp-s^s-oyk1r~J4`a$Ik{LV$Jfx}TghxmXw?1)E4=}TR8(?^ z`av=tT7S0h$>w8&bz02K!GKDf;h1n9?MN=Y$W7d4UGUQi6cC_MKX1R@i?v`9e)IXl zVW)yy#`C)8_x 8u%TP8%Gzu4 6(6ila$ z4X?+IoQ{6dervq0-4jqtIXU5hJP(~C%aY9+h7=I8tsL&4M*fPuo3ZaWuHpIEt_CkU za$*sNQVk#g?PVSA*X#Lx*1z5k3<;X~*z3~VBnwXb`Qfb!+fSh14eyfIS3T5T3H$g( z%a$kC9dni9P8dcU#d$yQO9J?QNxSjHeTm= 3tqH4Z!_b9h;$HSD^yR)U^MTSEsd&}$1zHn+c$)pC@!uB6k z)8bc@Yny7vN1KYf@d-l?O_!F=2JP!3lEWsm?$J}9x&2_J xH7y2MMtlj`Hab1k}j-PhsLh#6MFbRf-=4;Mnju~3az;0;ssbS+84{zf5YIZ86+ z_xh_IczoDClv7G$Lu^^y3$^l8mDv$c4^kM*B 76Hii5EXG zgaX{oWayIDbL<>-!bQfFJs11d&(in~LpdDiUcLbOTg)Jm_ttH`Oj_QwtO1GhuZAzk zd(*rUjVo1$QH3RV5C~ocA pyPo^uYzwd!oBW28@rZou4 zp=hY51&JSFU%K(0-o h^N6_1^>-3nN~&F@uW4dt;{` zpmdT4{BFy9Bx}&V|GPJ5(M(IW WG z4|NRri }AA=U&%(frZjw^+m=+zU8 ^+AXFb@yPu!ASZ;?v|{#s|@Il z2LQ3>F7ufCNixb;;%qH@UtoUS6CFJ #n>)#ehmTh%U$dXJB$sfxh2mxKCB{yH&3i3vC}Lv(AypT9sQNkRpWjOjUGQ06{; zjAn;~P;iwNeYa-q?N+guoW)BkX3mnskvEv4XKDcoxb`E({ AG-OqPFvxA_;Nj8^et4x<8D9xtlRh2 znkaSy66OIIiwP@~3BkL+VW3adX@8tm>~zvaZu3j(ZI3_WIGiS(;uT%G)JS@Ys$vuq zzlV^vd;ZY{4(9IytJ6Xo**bn>@62E ESe;w~jTCPw3c z(mzuf`|9SNPQ;N6{F;VtS-K7EUkL&J;qYd@?756#^(&LpUd#?38G d^^O6h>6Tg5l7N}FF{<6jMAB`+~?9jgj7 zKoqyR;C?sJx3{22FWfQquCU4@m496uzvab0h{X=@$q=;Es_csdeLPW?5DxkXn_Qs* z@%{^o$|m7q*3b;iGT|RsM7LqFkOf;>Sw7-=(OcO00K9{tyVmxR&qtw96m{x?-}uOv zBmDM{QlX_$?W05(3XTpGpuv3su8jGMHQEsFD~pPsm8IYIu+ad;kND3(k4kn! n zF{zA|@y9?gKmd;=49023ud2xEOS0yB&YV&1(zCYI5iUUH{}10!fe2m-kjYf+-qYKc zU0G6+lOB10%n}G;7aW4*Vro`X#X84D8IjX5y-(N^Hv!S3M>+J}V3HIJrLBc@@>y=t zgp1$$0fj3m3|;^7Q&TnB;nuIX9_Z@Lw*O7=Zx01nv|czO+&J`>>Q58?_s}62sKEo~ z=wZH?M-Vzd#!tTyiidsS0d~&DpuOOABz>U9UdU4s#|5cV2+BiHm=-L)$3xpp*HXR9 ziF9asvf7{e&48vIBm)**=V#z2Fdtoe30tuJN=oI{OvI(DbbsU-Su+^r0nvCjIc^U{ zeBjMVTK{zy!m?`vIpp?OXny9 _vjH*sGC-=>oe)Em(FozY-Ny6cCy=4hQ&|5+_=Di2)=kMmeKWCW z?NHEE(Du+8N^XwE;PY=y;@xYL3TtvNx1|7iQ?}NBiiV^0`S0Bo^XrUYdcy2=!U~Q4 zLLsGIF`>7Osx|DE3*abX08KxB6oY>i(o1bOt-*S;${qt@MSXtar+^03VKx6Pt>>+@ zOJi?%ynDXe_)lHhtk_exK}Z1=_+C&k{4DnA*!OJ3_l`Gy0__&~=uN<`Xg~>EtWqo7 zF@u@cUv$%2!RQ09^b!Vqis=?r8HtE&X3q|F|3%760w7vtj|YY-u|V6`Qr;K2IU~b4 zDn*7+12!rGKG_n0d+`W*MXlV&xoyE@NzA>&rcgWg1jy- v=4{}{=` E8_%9AI$lSjWVKs7k?DO z+e?0~U`WSfRQ1?UJSEQD7UtM(jt#X~G42mFqI2{_l?#zZk7hp^D;udP$1IWuDY6|K zQltAIk8JV2(ma>HZ{|jCOl*%&q)3YTIt9A6nM77B*1uyF?=Ra#zC1d7!)g^6)+$Rk zGzm3@I?F3nhBvTr!JFBWgxy`AbOmJ8=kv3}?8INf@d;_tP?;O9s=eFDk!?Wtd-eAa z1i0?;T(iQ}i>l~7EXs;4yJu>g9OfHGb9D+IO~0B7BB0 %kw8}vr)4JdhGcZD z(*`$VTLx{c?yCEXoE)v*Pt=;TvFO9rKq6u^Kls9lN5+_yH1Rc)ic4>}M~BO%BaA9h z!*hNgX_`SoFEBVU?`fa#=ts6<)Ua(y!|kGCmiKx#^ojTLs^{FpRnx=_iUgABmVUim z{rqc9r%;!+_}3#M v5MRZtg?My6Yt*yOOJ5QYE~m%Fo)!hL6Jn z`iFJiw?+s0ev q)@?Kj_Z=K{?3^h7ZT2?-192|~JdO-#77#NW|+Pw#+v zXG9xcIchr9Ke43-j<%;=zCdL&s=|I+pc ~Zv4JD7!1qm&-j%+G5)=?P+CJQzNDmUN_x&7x0H{?0?Iz^kG&nA{ zM%vien82j&iAfVj%rl?YdS9N|#Ky%z$<2uvCe;>1(lB%$%!d8QchALP_;WX r}anhMpcBCN^ zq^fd&zjFAa2*gb`> ZlHd!Ai LQ0Gr#AKt%n{srZ|ft~;2kgTWe7UuT=?D*K0T&n@afYhJ_!j4_Z8SxkZ(p8gyF 42EkjC50aT>|a%jMBr)F*42sniXCTUWJpON@YDu1U12oW zYonv1xA23 *ySKis;D-O$8g_tOm$s$gm9X}17BU1XmaTBdO9<;BH?(^DDZ*FHiw7PP@2DVPaH zS$i59PVx4o@EZmHwdOFM^f7Z^&tZ;Ymh}E`E|l1GgXZc(AQ6&D+OA20o&DJ>sIN;8 zXdzQ5na4pq@5z=-6?yl@7Gl{(Ak35ik&?0QJ_(O>1_ZEaJR^tu`uYegqU)NPd{{t; zuO%D%ZUo92P3|WzEG%pbQ3wLQe_$nQlYHcN4`X zR=qtv{f(!8K3-S994ol)OP9P3- LKg zaot)^Qioa!A{oo=TbU`ySfm%0ma>kHj+}A8TqGU;4cROLbcVd0ot>W4Aamy6BnE@| zWxaJ^jiI;xtFS^~1lsRK*E3T#ksTkws+dD~Y*UBeo!j2+M;;c;qJtL2!Cv(tb6&I( zAA?tp(o16$%}*N4F*RLxC}M~Lu&^XO?``<-b&1|GlH=1H7|)7sKpPf~ySus7;Nan% z2T4UE1f?hA;P@giEOJtDLj2od`lOL&5uGJq=>D^FLe(Aq%t 1xIVAnw&z{&f665dO>P>2wOHgHT(!gxjP2#hO zO*6W7XwP&u(@()kPM9A}BqTfQ30?9+luS%aMEEJJ4_-x<<~@^ Yfu_GOYjZWxJcHLH#;37HS_zic n(|32;t*DUVo&)u-b2 z^l<&-DXdx8Zr+#!ox22W$ZWYQ)S3AT4r?p#LOWYVj8M0=G2%Zm69vX7F0SG``B_Yo z`QpNZ&Q4Ovj%F=5L@wrNtFnAAzd&?zYg(ikb&i;p9R`;!K)zq-4x_4ElzJB<$1)DW z6Tx=6aMLT(`|CTE*VmWkCylM7f4?DRnA9vVC|AaMG}n@M-o=F{i39~}RJjaKifo7` z)15cY&bn+jhrid=*}?+HM@B{n=9+ODnt;p?!(+ {u>EU5xf05^~dCLA~ zb=|Sy%6^sTaoT8%>`ff5!qB@f=pPhh<=*~oCz|lHH7h!t5X!&*=f%x+O_uy!Vy-P% Sbrrf11}Mv`$`#9)`~44CT%>FO literal 0 HcmV?d00001 diff --git a/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexus.kt b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexus.kt new file mode 100644 index 000000000..85e467c7d --- /dev/null +++ b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexus.kt @@ -0,0 +1,181 @@ +package eu.kanade.tachiyomi.extension.en.hentainexus + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.UpdateStrategy +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.injectLazy + +class HentaiNexus : ParsedHttpSource() { + + override val name = "HentaiNexus" + + override val lang = "en" + + override val baseUrl = "https://hentainexus.com" + + override val supportsLatest = false + + // Images on this site goes through the free Jetpack Photon CDN. + override val client = network.cloudflareClient.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 1) + .build() + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + private val json: Json by injectLazy() + + override fun popularMangaRequest(page: Int) = GET( + baseUrl + (if (page > 1) "/page/$page" else ""), + headers, + ) + + override fun popularMangaSelector() = ".container .column" + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) + title = element.selectFirst(".card-header-title")!!.text() + thumbnail_url = element.selectFirst(".card-image img")?.absUrl("src") + } + + override fun popularMangaNextPageSelector() = "a.pagination-next[href]" + + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + + override fun latestUpdatesSelector() = throw UnsupportedOperationException() + + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException() + + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException() + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return if (query.startsWith(PREFIX_ID_SEARCH)) { + val id = query.removePrefix(PREFIX_ID_SEARCH) + client.newCall(GET("$baseUrl/view/$id", headers)).asObservableSuccess() + .map { MangasPage(listOf(mangaDetailsParse(it).apply { url = "/view/$id" }), false) } + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + val actualPage = page + (filters.filterIsInstance ().firstOrNull()?.state?.toIntOrNull() ?: 0) + if (actualPage > 1) { + addPathSegments("page/$actualPage") + } + + addQueryParameter("q", (combineQuery(filters) + query).trim()) + }.build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + private val tagCountRegex = Regex("""\s*\([\d,]+\)$""") + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val table = document.selectFirst(".view-page-details")!! + + title = document.selectFirst("h1.title")!!.text() + artist = table.select("td.viewcolumn:contains(Artist) + td a").joinToString { it.ownText() } + author = table.select("td.viewcolumn:contains(Author) + td a").joinToString { it.ownText() } + description = buildString { + listOf("Circle", "Event", "Magazine", "Parody", "Publisher", "Pages", "Favorites").forEach { key -> + val cell = table.selectFirst("td.viewcolumn:contains($key) + td") + + cell + ?.ownText() + ?.ifEmpty { cell.selectFirst("a")!!.ownText() } + ?.let { appendLine("$key: $it") } + } + appendLine() + + table.selectFirst("td.viewcolumn:contains(Description) + td")?.text()?.let { + appendLine(it) + } + } + genre = table.select("span.tag a").joinToString { + it.text().replace(tagCountRegex, "") + } + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + } + + override fun fetchChapterList(manga: SManga): Observable > { + val id = manga.url.split("/").last() + + return Observable.just( + listOf( + SChapter.create().apply { + url = "/read/$id" + name = "Chapter" + }, + ), + ) + } + + override fun chapterListSelector() = throw UnsupportedOperationException() + + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() + + override fun pageListParse(document: Document): List
{ + val script = document.selectFirst("script:containsData(initReader)")?.data() + ?: throw Exception("Could not find chapter data") + val encoded = script.substringAfter("initReader(\"").substringBefore("\",") + val data = HentaiNexusUtils.decryptData(encoded) + + return json.parseToJsonElement(data).jsonArray.mapIndexed { i, it -> + Page(i, imageUrl = it.jsonObject["image"]!!.jsonPrimitive.content) + } + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + + override fun getFilterList() = FilterList( + Filter.Header( + """ + Separate items with commas (,) + Prepend with dash (-) to exclude + For items with multiple words, surround them with double quotes (") + """.trimIndent(), + ), + TagFilter(), + ArtistFilter(), + AuthorFilter(), + CircleFilter(), + EventFilter(), + ParodyFilter(), + MagazineFilter(), + PublisherFilter(), + + Filter.Separator(), + OffsetPageFilter(), + ) + + companion object { + const val PREFIX_ID_SEARCH = "id:" + } +} diff --git a/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusActivity.kt b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusActivity.kt new file mode 100644 index 000000000..5ef34cb8b --- /dev/null +++ b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusActivity.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.extension.en.hentainexus + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +/** + * Springboard that accepts https://hentainexus.com/view/xxxx intents + * and redirects them to the main Tachiyomi process. + */ +class HentaiNexusActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val id = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${HentaiNexus.PREFIX_ID_SEARCH}$id") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("HentaiNexusActivity", e.toString()) + } + } else { + Log.e("HentaiNexusActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +} diff --git a/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusFilters.kt b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusFilters.kt new file mode 100644 index 000000000..4043f16df --- /dev/null +++ b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusFilters.kt @@ -0,0 +1,44 @@ +package eu.kanade.tachiyomi.extension.en.hentainexus + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +class OffsetPageFilter : Filter.Text("Offset results by # pages") + +class TagFilter : AdvSearchEntryFilter("Tags") +class ArtistFilter : AdvSearchEntryFilter("Artists") +class AuthorFilter : AdvSearchEntryFilter("Authors") +class CircleFilter : AdvSearchEntryFilter("Circles") +class EventFilter : AdvSearchEntryFilter("Events") +class ParodyFilter : AdvSearchEntryFilter("Parodies", "parody") +class MagazineFilter : AdvSearchEntryFilter("Magazines") +class PublisherFilter : AdvSearchEntryFilter("Publishers") +open class AdvSearchEntryFilter( + name: String, + val key: String = name.lowercase().removeSuffix("s"), +) : Filter.Text(name) + +data class AdvSearchEntry(val key: String, val text: String, val exclude: Boolean) + +internal fun combineQuery(filters: FilterList): String { + val advSearch = filters.filterIsInstance ().flatMap { filter -> + val splitState = filter.state.split(",").map(String::trim).filterNot(String::isBlank) + + splitState.map { + AdvSearchEntry(filter.key, it.removePrefix("-"), it.startsWith("-")) + } + } + + return buildString { + advSearch.forEach { entry -> + if (entry.exclude) { + append("-") + } + + append(entry.key) + append(":") + append(entry.text) + append(" ") + } + } +} diff --git a/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusUtils.kt b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusUtils.kt new file mode 100644 index 000000000..ecb9547b8 --- /dev/null +++ b/src/en/hentainexus/src/eu/kanade/tachiyomi/extension/en/hentainexus/HentaiNexusUtils.kt @@ -0,0 +1,59 @@ +package eu.kanade.tachiyomi.extension.en.hentainexus + +import android.util.Base64 + +object HentaiNexusUtils { + fun decryptData(data: String): String = decryptData(Base64.decode(data, Base64.DEFAULT)) + + private val primeNumbers = listOf(2, 3, 5, 7, 11, 13, 17) + + private fun decryptData(data: ByteArray): String { + val keyStream = data.slice(0 until 64).map { it.toUByte().toInt() } + val ciphertext = data.slice(64 until data.size).map { it.toUByte().toInt() } + val digest = (0..255).toMutableList() + + var primeIdx = 0 + for (i in 0 until 64) { + primeIdx = primeIdx xor keyStream[i] + + for (j in 0 until 8) { + primeIdx = if (primeIdx and 1 != 0) { + primeIdx ushr 1 xor 12 + } else { + primeIdx ushr 1 + } + } + } + primeIdx = primeIdx and 7 + + var temp: Int + var key = 0 + for (i in 0..255) { + key = (key + digest[i] + keyStream[i % 64]) % 256 + + temp = digest[i] + digest[i] = digest[key] + digest[key] = temp + } + + val q = primeNumbers[primeIdx] + var k = 0 + var n = 0 + var p = 0 + var xorKey = 0 + return buildString(ciphertext.size) { + for (i in ciphertext.indices) { + k = (k + q) % 256 + n = (p + digest[(n + digest[k]) % 256]) % 256 + p = (p + k + digest[k]) % 256 + + temp = digest[k] + digest[k] = digest[n] + digest[n] = temp + + xorKey = digest[(n + digest[(k + digest[(xorKey + p) % 256]) % 256]) % 256] + append((ciphertext[i].toUByte().toInt() xor xorKey).toChar()) + } + } + } +}