From a5b1332f95065d8ebf4ad372d93c342d1bad8b94 Mon Sep 17 00:00:00 2001 From: JigSaw <JigSawFr@users.noreply.github.com> Date: Tue, 5 Apr 2016 14:52:35 +0200 Subject: [PATCH] Optimized & Fixed FADN Provider LoginCheck, New Logo, Optimized --- src/Jackett/Content/logos/frenchadn.png | Bin 25425 -> 39306 bytes src/Jackett/Indexers/FrenchADN.cs | 1951 +++++++++-------- src/Jackett/Jackett.csproj | 4 +- .../Bespoke/ConfigurationDataFrenchADN.cs | 9 +- 4 files changed, 992 insertions(+), 972 deletions(-) diff --git a/src/Jackett/Content/logos/frenchadn.png b/src/Jackett/Content/logos/frenchadn.png index 960e0368023781a16f262792c9b75821e03266f7..4bc6b8d9d8b9307e67e0ce8a3d9f1688d712e2a7 100644 GIT binary patch delta 22754 zcmcb3jInDolVWFppF1y?6c+;n1FxrtOArGCqaXtVLm~$o0|UdygHwM`R1{}&%A9D` zr)s8~Y+z=tU~Z{rXlib5Vy>fLWMF8jZ(yu%Y@lmoY-M6#Wn#4PZ9fyE>Ev4GCIJ&& z1Je*g6DuPND+3d41Eb0OEM}O3p)5%%iH7E>W~L@-y2j>7rn<={CMmi}X+{RR$;Otc z$w`SxMn-8Sn{TtUaS)|)vLT15u$is_hW%%G{TNLrJF**0{=m*Pc@B^L=Ba$TObV73 zZkC4TCZ<MC#zsb#MsCiohOUk-mToS_X6EK*hK7?1*%c-q6bPLBkCjW)t|GTUFC{a@ z%GA=$$->yg$;HCO%+S!))XC7*+116(%*E8y#LU9gVsat7*k&QY1V&~fbK}XSLgq}C z=9||E<?FL_nQhIVtmvdyA2!?5#WAGf)}FnU8**=l-uphg*mSq$?=Roy-TuD*;$`6~ zA%7<~j*|%o5`-TK1Su{oJoWTO-^O4*v(#08+LNcBHoLK7l9z#*;S_5PVP_|v8)6bI z8*>yn7ruM>_5HWIFTY!+m#>~#{H%ZHwW{CmwqK7bo?HDiw)9m=_4>4Rmd9o4A4}i= z(Z+H30*9i2Q$mkoJ*Ogr!7&kg|NTp*^zW(vWqf(Rf4$v*%m4HK*Z%sy`S{H8GcT{d z*|_Y#-cp^vZ@&C}_T8v@Zs>Q7r7qdAbD}j{rwE<@d?70Qp8LO<f25Y3xBa{CkKB?e z8cXkel+;vp;@EPnV&3-?=~ILH=V;g0*X91KPftH@|F>t^`t)D_fBt{gd-;v2-+x>A z^*4WCjY{r0`2FuAyXD6Ven~#5pKo{l%l)+V)n$I?rKkU~{rUCjcjLb||IAu`-qiYj z+Vg4Geod@2s{XtAv7PO=%jZ|UtJz#!a_@fHdDH5(pZ|rGoq73RI(foo;m3vXp~q*H z7hV<*Hs#PeUcX-YLuGSB`hsV=ODeA$oxiXvCitf58L4lTZspe=g!sPL^xWWl!kO72 zm!Gefn!f4didzL~=8c<rSHGE)TEBX>dg}F%tq!M;-FX^mvU8eR@t&N;Ym4|^%?qFW zQ)hKqZFsU_?Z(e)^;dn*R0*g4&)>vxnr%te!Slb*6dp_dR{UA)bp0Ep!kK4M9|t|> zHIr<Ax&L|G9h+pM^U7<h+cw`jzlYQA^}(~BS(fz&m-%nDe!9mpa%;!3^_#QiO}U$X z_gIkm`e(Ufq1WTe->kBHnRVxsS&|~lrmRJwk%u?U&74;qle#mO?N(`^>BeiN_X1`L z&ar)#bK=UAs8==(YP-+vaSayts=wM>8u?;|{-&F<=dTtQeB@jGF1f7lYU{4h_xjw| z)13XLZ>YV+Vfs<+RZrmD^!TS+?K?{Yzn4drHyND{OHb%~t#p<z=BnQdDX%L#mw&cd zZsn74kI(S^!fQvTeHL4MdYy58<IWc8(vKhCeD#q&dGmMCOy<vK;?w8eD1CRPO7`=y z`t5mn&%Rsa^E_{5FHH=;xp`K5_nFT=vEtQ}eOJ7>8CtotG<(4Tk=U3en<igvj0ojE zq3W0Y=$GkH)>V6&wcey$JGzGH>#C&hR^>I{qL%$PdwXuW<c62;Ka0J$DX5luaz^}` zjl$-0&U3^MO{)~V{9xPZbrEgn&q&{|d>p&Y;+50kdZP_HG~Weo6Eyp{?zGePO~1=O zzUdcE6Ls5hrs`_sja_$>=2ch4oQ`EbFip3^Dt`sv9c#T`Z-wShtJKkJc)h39Vz<QW zLu+<<KHan;ZU3uthWyzx_g-7|_)2e}@b*1AF>601#T0f_K7Lg7R<CV|-k;2_xuyrN zJxg<p-K??h=9FcM^**9<51HDgEiI4Tvp6!~{L!^Kr(@3SIu?4}PQNyCTGr~>CrT~+ zg5PA!`?&J3(yVu9N-H8lGYq?nuQYzuyXSgyk-oJ5^fm5xZ&k=72X3znZQZx|hVM-G zJ1ojmkF%z{()qLJP4DT#Q$K`D*;eGFmv?r)mg{;gaYky<rjMGRr>s(|FS`0<);ykd z58l}9J7<%VAouJ+Q}G#Vx5-noB0WO3R-~V|Jo|Vh^ODmxcHst_J)IsEPGU|y$s_*$ z@X^KVZgV_an;F^uYj+CUGv0mKYhD(u(^`K#)qDl>;g}QVrQf5sJBP_G*2;c7D^;_* zJJw*<OV{veGc^t$TIDp+NM{k3vPN~iU#LTotXt62ty9ikzWMq5yIY}gryr*X^-s<% z{P^b8hU3eAbQG5zQ?-3r6`X2a6U%ksX>sY!k2jO2GN!C^Ta%}>yC-n!=}56fp{@L- z9#x{#g2d<f7zd^Al>2<{rPqYn>~i6f$9Aom>g~1NQzN`JbBgV1tvM%5kE}8Jq9?bj z`+I%7xSqM8)+U}GYc`!)_hh|R`3BE9<uXz43VD)+{a5mxUisK>$FYy0Ec+fW*%50b zv3aJ#^vCCnjY@rPMouV|(w<eW8|t=;%V+-dYb8$|WUoiB@Lnv}uK3PtPVwC}F=lSl zCwT6PI;>?BEF9GB{pXs=)vF>$*DrFP{!rUjuIgq>ZvCOjn$a1d$ChPn*ZExb%4z$} z<WSu&YBPfFnx5tE?PtETdy>@bDJC-QomQ_IcN-+dKKnHJ#J3&m)?c#Pq_#0Qvg&H- z#+|#8N*8+0&5kz<e9d8|*L@=O!fA(tTHE*QT+z7gxoVS!-tLck&ie)5IQ^mP=I3jz zGNEx-bp&F!a$ZS|sz0@-XV)2zneLHo?xtrQrlyw1%nzShSbR2NS4EQEor#-1PAI)r z73bS>+Ao~-SZHH*srhrJ6`_sAzP(YgwjQD%!k@o87nd&UQ8}mJwCcOkRMp)KTQ~1N zAM|APhqg^u%J0@r7uDXC6c<{&`z2qzX@9J-i|;k}yIXfXlJ?!S^8JB&mzYfoq0`oK zRb&NeKYrzQ^`Y{*9}Y9Q<6JvdD``xv-KQt~<khe9yVIwi-@Pd#wqRam_aW9ZB`43B zF8mn&_C%@6;Z<9TA`=(9o>g$y<khoBkC~ROt5FVLvs!Pb;^{k%(>-4~ebRa?b7ptu zbsdk;v_*PPRz>lICeB)|xMHzM_|BvCr4sH}3s3KHn;v^nwRBcB*XHwCR>j8oYnEqz z{%o^W>+r*%3z6<S>tz~huQ_WSZeQh5w{FdKHMxH8YwPurUphT3Xf>+|ce%l8QM`1^ z^hz_mdtRXy0j0K~lFL@<9NH57t}<?;gw1(vmgZ&ZQ{VsY7nqwS9`J8}Wn@n#vq9|P zr1~C5&95hz)~#~X(muOhS?lUm{+(gl&duAe`L<@>jew~Wb2jZrd!HI*aboGCJ!{(1 z?`<{nH4Cxj57)m}+IRl*jMvBZ<@o&7`d(6|xZ};1byo_-|MpcT9{OT>ZudSlj+#)( zUtb=5x8cA4J#%Z+(eDO4oJ|bpKXZi|Ci^7Dq-`~{sXt$RoX>7|@_DIgHA%}ey=E;t zQs^^DMa9!e#I-djJXGT0B*PV4Ggr;~xaa(t)AvFuxj*-ufAiz2Ve;uiuOg4`x|u!m zo653Y$GOj~&bF*QrE`sOf!-C)>4qB2>t<{1-+86^>}>A3yNh&pUwB+(^h&KHvf^Qr z;Eo5Jz8zLO8#xY7sBdj$t=fA}`M&jBzC;PJd+vEMY2uBq-`UPRcXluTQjtIrL%Dv* zUN@fh!;iE*g&tYV+bw)9Eo7Ovd*lhrxw<n`*Pjdfyz7DO=Xr0pw40t6)}DPruhcPg z&yr;i3WfI^D1A6JbwOHh{?&8#{+lkdp3v8hUlC<r8u!Hd&`c|b4O&m@L#CZCcl>_( z(3Dj%Rly0{&Z$<nr=>irWpQ*^emOBQvGMEI(reeQMa0La*VfLxdNs6aZ(O2B5wn56 z=<3debK@d*em9isx3sZYa{vA2?c0yPd#CsN_ir9H=C;F%fg%%^y^Q&LF0%Ooi^!Vd zr>nE#UTrB|e&0Cv$*EK7&m{Nms$Nz9m~-cqsI$>KQrRwD-K@uaa`m3co4y7dwJ6g) zryYHGUGdfYp#1^j=XXBR)@xhtmv^ys@AZ~nHro52|F)a!*FJTs=(X$DEz94@i0j2X z`1AAg9Q*oty}i6);o*@H5eu$ni6)+zT=8_F&#YxWYQlYeeK&63PA)E<T>brB;rn~G z@9x*{KmYC9H|1UOr7CLSX?c-aHF7JgUKg6q><B$oQM&TS%Yd@DX>QyG|WAYb^Bn zS>jyvn@b|T6gpn2%t;D2)SYf{;^<<1*09*Dq?oIdXWem_Jolh#%Sx;Bdl+Y%Da?C* z@nuOwTwGdma`Wlw`k%jC_WyjR`25E`Q|A2rar~k1{DyjVeuZHE|5<ydH3pwP{ozWm z%Kd+D_xvo4KfZdo-^Lv~JXT+=I?~W{JbC$qbE%7GnA??#G`wWBc^^5o()2Q$qh{;- z-&>{Kt{$4d+#~*}<hjFcv(CJ)yt(e?@70I$UkUO0u4LY<n0495|8nV@-S0#$2YoC3 zxP0};LdMUt(wbMX)yCDov9!Kvdw%iNtc$O|S`<EFnXVuI?C10O&;P!!x7W9;>G=NH z{=9%h{g=fngw+iqk3C!cNr<V(k&lh#`}@m(kEmC?>ovFdx%d9*Lx-3g1w`b$-|bcL z4B8T_aac3$V_)UF{F$e9m#X+)jyl<v*-$c1+>p0QZu)|V>04KG$1XR{53PUsrShIf z=!}SP^T$mur%K+lVh*|bFZtSQi*=g~Uf=zxr|_h3lkVpOhpe<CM899Xd*(~twIj1< zP49eJwtLOGbwv*kvCgag78zIb(bew9v)L;4KPTtZpWa^ZCHKBt(L<)H)T2@p)vx3& zzS7Zi{6}<j%lf^?KI#8c%C`R~{HIa=Ut)d5zsg4zYJHEH>`(r_zvD&EIpaH_H~T7u zPnfn^&(@t;o40w*T_XpB^9@hi)|`2;v*Lj7l}oEPv{^--TlKLnX3l!fvOxE{8H?tB z>QFa2pK7(P`?E#Xn{~~htT)aUKVM}t`{wzIdG5RKrWF=W3|{V6`1F+My!wBYb_E3v zUxMwA9<Ja2Wx7P|6XgV+dj}amy^H;~dCwA;aw{JDZ-$G?_s@PIUO%yS|L5|G`~80< znh!P{K75!d<*e;Q6JL48%Jj~-&n6M44_w)_Yq8bSZL3a39J5)cbY^O<M`~ifw%gS$ z_Pwj0N2kZ6YjTCM*86(NuG-^uT_^qOteK|E>TVvr-YOT`U4L<^U5xm1vy&-1Ry2km zom-h@#vXirj^*L4+1D!`-m$zg|NrNlf35oud{``h^#1M5799csLZ@srk3CWT?=AG_ zZLZJq!~gjIFnoH@F!}!v`Oj~@*UCpnN56Udwy>bULB!Q1FvL>u)B)$|Nn2m0PpM}4 ze60SO^s33`D?a(mUh+J>uwF8p|Jv3y2ftcgyBj^Tu;AvF(>#@z#RPpd|6G1Q^Vq~c zS_e*VU9?^&_DR!Z6Gq0@J)XhNM%k^04RoAlZcab{>B>~?HGdE3Z}@tMo5%i>;GR19 zE!)_ZBnh!QHhi(!V%FgJ%#Hiad8dSBa!xb9)*p8fViG()Bd-77_j>!i^8XU&)&E@g z{?WCg-8K~;8s^TGWou@feDaB)(5*e57H1de-e&y#{fUi)qD$l9Cky8Xt$MJ^NBGGr z717f%Pp$rB$DO{gt9RWxlSO&wc9@y1UOh=y*?rfilvPHqzSWOq)N?d<$NjgEy*%rI znE#>BHB6UZ?P}S{SF+EcYOmbu*RSVT7PqZ-s-N)hlfLnvkMiy=f=ZrRydL`=xE)q3 zlu=yTK80ziNtASpNXAlwhUY&y7=x-R>fDx#GimO9Bob7m@Tn-fQ9yygRBrMdyR#3E z+Z|gu|8L~E|2yrEe|>#DGAb%5KfnL*;lt%$iq4A9&yJYhd}iPK%X!;B*3LKJ$=n*% zKZWs0$_|x!Pp%j>-{46qnmhD1-}B7du)O|!{k&xhpR;M2cU-<c_y2y)&pZ7@bIX>O zdd{6Dx#i}DbFGsfCxxt@aU@<g!%s%vIymj9{PcV8cAa>wlYW^mL$B6<nJp-(Z~Sxf z{H6NqyF1p$x%#PXWpm+ZyfkZl%}?$nhZ*-CbzSn@P{P+^ay`rW&lXaAo>LcHj&$JY z-ypl`Laak}>+|ZCL~hMg$CAs(9{iSj8#db{dBe8bxrg$mKXkYE`egoJ?Rm}oty@jS z#Km8hSg|;M=qNXM8+zt&cbcW}&!5q2#om_9Ub<9OOiZjx|NeLGIG;%>J#Na2G?I*F z78Dd5D6mK|nkjJn<Kn#f{AU-RJSY`lY5h1a^>x--?qlnMR{gN`6xwp_=*IMJrsZG5 zmh}clo<4U|YU!>@du#vV#pe0SvvZT)P0F+HGk+KKGg{p|YU-l(%u8yguQ~Df+us61 z7Cn(P@h>~xS)?|d`g!PO*=`*@y-oY}_5Bq8-*l2&-{QXi?#mCKPTzN?dA50d#lM-$ zU+8_2=s&(i+)CrJl4tO(drV&0lT2Jy1wS4XT)a1TidRPxcb5{&ye|{xsrZJn{V21r zuu9rrf1o@5yW+mI+}<x|B-OgR*W~7H+O)}O{dMhE)iDLDDowuB1eROLPMbC@A~N#f zkxpUO<nqeOnPt0u1y~q-)P&m(E3&h**UQMri2OM7@83V`L$kwHyRN>f)i+o8Ytg2A zUk;rKzP>Yja{berD-I?ER5)DT>az17PZPua?;mUTEAadEyPo^d`*_EX+A6;%-_5P_ zb~?^}I(<iyRH>2py4S1yJojrIzLqj~w*}+B!r9*EmxZjZkkEVRGPgBHXx@~UwPBeZ zT#8+f>UYJhKmO^H(X(T`kKX_2`;+q3Q{;Sdesy<iShqorp`5?RBZ;-Vr&4w=Gg}d| za5Br%pi3&QOJ})gUQ$u?6+U3#6%@2o&~3pMr<Wp)4;I*ONDP`aY0WyJ`IgN)O1tk} zU0s}A`HErR%`Mq++FQ14IdXEcx_siRc{YVj20SlItk$fnU(3{#Vf66BhY54<#>&}L zIIO>}T`>7h_le7!_Qv%uU8;KR+O-=uZX{Gz?le$Kl&}+8_JmEv_cD{tJRh|lH)XbF z#=mv*7cT}e7GGSUQE=w{%F8b=<ZXXhV%56v)cn%E$10wOQj8LfW{NzY_3c>N;r*G; z)7rCtetfkmfAtQF&GlDrulsyo^=9DpCE4HhmYNoSwtIiQaZ;%0Y&{zx@m?SCs~^== z%)+<lZws+ES9)7EJ8ygU&D+Yc@#}e$i)0>tcrc;FCM~zL)xvY8&#?@i%t;HT98BQ# zn6>O$gGR5JrNT-9jvD(DYAYNVR)#InI+&3iu#1~JW=92MBG0bdu4~uTAG#+u_xb$g zSF>z|q(T~NQ+tl>xlvGBI<@%uxsPXz&lmjqlDSRpPV(n>zI?yiZ7O~=<Sl0p+OT7X zM}A^sp~%E#T~DWqp85ItNZ}t3kIPwGL22#E%3x44%zL_C;jb^5)22^fdoe0TC8sqb z*B~l7U-xiik@BSFo4<eme)HbF`n2lm*>~^8mX()VT3IQ1PC7I>DlRUqvU2B#3%l;V zQ&x<KyI>)~Bf-PgbNn&W{*!xW?D3M;;5nCI@ZiOZj*A&B0*g4-r``R#O6&frH%njN zKXt9MTc}R<_U)|(A1B_L`8dsE)r>+thTiUDj<GzE$7Kst?><rVwW~a2aO8+#N?vaL z-P8O`@9yvS7wUbfw@y57k6C_on%3E}^tB#ak1@DO2dy<a&$s2$%@i{Uhm|60P3Ao} zmh_LfYvH<n`;2Hk9|4C+B6256B7)q$>YqOH#AfExr>TB(t&Uz@9bWmq`u^$qA8+qJ zI<`9g;hUMpckbVL^u6x=_uSmT?cG7wl&^YC)H+&kUjKS~;n$hzFTVf&^82q+@Rl7r zA`BYz=h*W{6+ZqNe)IOth4lpoGK?19etRiv>kW&IeS%FJt|sQz^2U2PDMm)crG5O8 zxh`hknTrVr-rnB6v-mmR_jh-b%gd)<%xIZ1DR2AfW5?K*`_E5HPHz7CwRGCFX<Yd# zT}pj@eJ{TMe)GQm{pS7q<0Tf}5q@1_RbnMubKgBXR>bk$n>QP_ZvE=9!;DwXK2O?5 zeCGLG6Yg&f$iG+FzgXzS?k2<1nh9F|i_f%Xt-AAf#|`gTsft@mzt{fH<v;Rp7nA*i z7Yoi87#x^mGqF;NE4gIi+(0c?X=aX=F9w-i*SJ#G9K7H)Sw+(;yQThMTG6tGV-~)a zJ2xi2_so_~N?mzBW?O>U-$*vC^RpWq1vYHhaNyQ0spseCKcAUC&vE6H@O?ij&&{`P zog$aN=ijv2_~nNaY>sI(e^EK6vBga1n@-^Sb91drcE|Sh^%)%7uw~1UmzS6CEPE@( z{=0h5r=RDGUSHE)7q|D=w|8>u_kXNcJ0`K~F5g-M6<?vKw`PvoM>1|tbC8&jc;IAk z`i~3k_GgOE+sC)Ket&=e`G*eziY(sV-gB(W`@U{9y>>m!b8<`j_jg4F6)h9wPM_8; zv6i)3Yxb{Gc#duLG>xu~$2%CVi;IiDEVJ@Buk0(#$KQYB)~!eH?(RM+@%&-$SDnzc zj`bFP`g3;qq)hg`r)pJjP)&QY@o~)s#b;;j5a^GYQT@3@eAWxAD;qhUA3dzcuk9)P ztNM4xn-7Iji>ErJyQu_nt>p>gvbpcB6fm!yLq*NgQ>pn9SB<Aa?WHRUOBEMSec;)j zVe{}#YxkC0x|dWc+tm$w-#-zq4Rc7p$7ZNoJt4VXQCvUn$<6fnPv`%6bN<5HEg$|i znCIVFcK?0xrzci{lO`?e3KMwnTh8mZhyR8hSCVsc*D_RozFVJ}+1cC6dv3n{`QGbs z$IP1yI6l<Oe|tBU<-zZHu`>c)w0R6OPd%*2S#QXbTVW#k`P$dlH6OX-AK1RHmcRcu zzMfxy-w!2iZSDFu@82_T*tV@Luxq;gf6G2LcOEu(kC&f6tzK_DtLH_T_=~dLKZ2BM z=l_4U|8Jb#r{4P>GRN=itexHb*S1W~E}@{}M1sYm4=*NY^hxlvsbrp7>OH;V@r=cb zm5)6(eEe}o*oKuo=bx;~e;jq~;V!-Y%cTpib(}dJwZnv`=wryLJ6r12Sd7GPHr_M| z4Q)-G>;A~5V_*Je;orM;zKI#!nsCI7$8)M1N3+9&pOe=9=D%6<LE!%P4@=uWsI0uG z(s}5!$}xvk!IN^*h1r4@drsy^Wl=Zxdstbvaq7aapZD}d&PfrAIoIo<S6%S*l&IXD z{pauAzHYg5m)k0y@^^RCc$~IuldBK1=GmOny5)}`PkXz&>nh9VQ!D0$w|H>h)%$xl z_8UKc;H?`vp>r;r<5_GtD=Z^;|KgJ@imLM%7K${A9645$_UPmc{eSn;Ev;-G`2TzP zzf(NEW@0<Pd{WQDCnqP@d@Hv<y7#uZg$xhJVX@PvSNFI(t36iCG)b%XS6*9xSbYDF zde*uJ?DZ24A9ntC^8BCpJs)44=4n&DwQ%jEx|dI<bI&__>XuafxB7ooWo2bI?%jL# z;ll;a`eM!P<(*71za(}ZU|BbB_vvHrb}tEdUG}Z=7Vo-<(>Zo`r<y#wmA1&Q^^a#n z^7-$UHWmp+Hc7d)b1nP=JC#hOCNB>#KL70B-}X0o_3tiZ|6HJ8D)+>+hUrqs#4J`f zWkD6sE_Ic-?T2^U{iv(*-s_%JaQ(u@GX>A)^oOlJdg+poy5F1!hg!L9zFrCbbH@IU zW>_`%mn!Z%JFDkEyeRm#j6IPh*IvP_$-|$cx#7U%BazP*-F_?7mYC=gwN~u(>C<y8 z3Yn65nhz^)EB{{4aj4;8LCRsXT<2XEK65rVxMZfBQ#5Szlv!-Vds}JiD&F$^J5QGX z|9Ag+{@)+{pAT{CPgtg@efspD*Z<%DR}GtG61XL5!{+UWGfbQ^dqpOhTxD6PGGWr& zv%U=5wr}52`B(SR$(EB9F1znOOURd>ysTxdn8e|upT#5N<Lh0z4yM-kny`AsUOo2g zs+sM&FNwj2X2zE%Z+?(EHT#Xz?$XejqP4%~PS-9MEzW!;JC)C%>EHv7scNhjy;Bob zUb;4+C8(gnB<1N7&A2}w++VcVUDQaL6tS<##6ZHo;}S>a0duy+JnfGK744+O^*-ET z-{0o9`)}Wm-KR>|esWrN@JZhG)AhG*NiFxE|LoDx?wI=5x^j1R9=-KR$t&~S-QE4~ ze|l(Uv@-@yYBJRMmQ!Wcm29Kgsg~TNpkX9q=`nM8S^2i@lD+P4<YhK&+jp+OU`yO= zPtLy#imWSxSmyPgFp!(8RnjKPy08C~N3B|@&zBk7<7!*$e{%0X(EsN}KU2hq;?Iip ziyzK7nBlYhcB+Mp_|*L?gFHC*J%7x3@Y&z$Fw4CC{a#6JhYQ<^e;x^JQCAh3?f82a zf9(nn&+t&0-J4?NtKXSUV9Ry#%N7y}^<v!Nwau3A_qsDr%Q`im@67x>W!tGsvHp*q zdY)97wPEwm2>~}e?fa5LrmeaX)y$a85*;0F^Syqn{1fN<9|Wg(ali9?)*>R2SGbgI z%SDcR->yx$)O+dX<z#DXHcNron}hu4*_{0O`T5SOuUhuMFaK}wd(iyl#;tpa_4)mN zyIa4oth|`9l*6_^Ok$BS^SkOv7c)XC%pC<pUVb`y)HN(D?8l+!b^J?3AAgj{<ym}D z!r)qig2yA4^Yz~q{U**!6LR+v>YX4^#W-(@O60@%Sxw>|O!pse|9?0CW3By<W>9^y z_^|V~ZQCS!+IWs9U+YP<nYCc5+6UWg5u477Y$`T29~yR-zb`5&IgueUaS0d8kw6!l znF_rJHC{``?JZlYux@Io)Ag-PYhS$&R?oa~h2hFozEID|T@v-;$vm4p5B74eUgz^~ z?V25Hd@e6I)f85HZz*$R?9=?ovW~y(nh!SoU8_57*WK-!w<E8q+RwYz<tejy%O<DY zcdK?U@Z(q_z%es;t7_1V_fB5Bf2S*C3U7WMwZQasYs|iynY+v16+HhXEhciU&3gO$ z3!iGmta76gxp^vXNX=LA45(ju>I3UZmB;>1R$eOGefH3yrlX#>a^>#qb{0A6;+gdF zU#6EpMa3cIKA{%FId+w8;`{%-7Myo}?QZ_cr^kHr?$w`oSis>X{L5~keAJqg4F;3W zvVZ^4KmQLuLr$Juih+cYOkXZnSSDL@qTxO>Q8u^8eSMyy>-T-)Dt~uJu}gVMePG6k zh@+fxO%73FACG!Z=i2r<=dWMre--goQ}H<)HD}z~A{iz-#~}ahNwwWKbsC=Ex^Zf$ zd*|xVwWogVm~i1$l#p3%K@i8W$AX!YHr$9#5xU8=WzPGFOEhn3s7)5!yEIj-%Jy{9 zS+xwumwoq_l^iehnN%xkukJZbuFP0Q@2g;aWnpYz_!E^4{r_LbJ61J(xmmO0<bwwd zOOMK}4B8T{J8hS(jb?ya@C0$m#n%&z13iPT+$*t~dq{Yr<eJ#Bvht732ilw(```bR z&|#2mW0e1MqW$OT`nT&pDc65pU-)omxnY;`yS&`bx90Bt($B`i5#{Ky{PM9`Vb06% zKAO6|hPA$d{rmgHp@%HyMZDVm#lTJQhuwsgYc+GVH}xJcZ#!S}%Kp<+{aVJQrjs=` z#Yr)(G?^!`rFMR|xL(n(H<50O1t*_;BFl6$)pqgo)Vx_8>R11+KUZ<JFyNF&_PQrQ z`#ZBsWd6Bc-mz{~#0oFn)2iR*_)eV~yL+|zr0=zsRu&6nE9yP&C!0v@>5Y7nw@-e< z-rdJP{bI|0{n~8vr1-<aPYz!(Et~bP{@j~0H&>>goAdC~)6;i$mUh2hzrRbMDew66 zcWZCEOy(?H-5VygCDB5%S^Y(uZjay+E!U--iK~03l=&0~nED1+?Un29?zY)c-v9Qk zY}xnUb1W(+<?TMI$kfBYQa`t>Lu69={Q7xMpQi468NIJHZ2Rf{e<%7s{5)Sj?_kD} zu>TFMVdC~5GOHEmO_>(1tn&TcJ>|aScX#)muCP(-6Bets?Y>^It<O!m`ND(Ljf+p1 zHJz{jb-85vbk9puPO7ApnmVX(D*19Ob@%=?XLkXkBFo{26E0?igy@`Ky>Mo3eOBnz zO$%3+zl^%}-fR7eETOom4>eEm%)9cWG}p9J=4_J{bJ*0Rb(@crPTTWt-(rij^6YMg zM#se=S<aHFC&MKIRz7-=ab%@&y5-J|fjSRlYQAjJuYY#>^aj@JCdKEr%stoh#CO}L z;<MWhpZF;a8YBPo^z@%=`G2f3CtY}_C*WVN_U7$70aecCgB<ZyJZ;Q*c01bLF63=} z)3;7%)#9yceT$remtKA;!j@85IkTN#Uf`(Uu>*&krkgD8@z(s<vgJqDwK+DGn|7?{ zum5ql=6U_k_=2C8SpDW$9F#b$(7RTAAx~qztt9`+41<*V`uXkr@&=tsOGLID5-XfC zVVRVFMLo+t{~7)3)AvpZTYYpx{ef@Sruk}D%grt0mR0A-<*02mX`8(Ke8PeU?N8=; z#7ey565LU@Pw#6~xo-byJ^i=Vk2fW(vN{*a`Q0o~syy;csLixpjVrW#vM!a_s8^^K z`qZblA5}=06#7s~*}yGXAY-xTB#GX{f|4V%*Y7>%xIez0VfNg<tFI13EZTi(*@DyU zjt<N3CMITbu6Ox*Hs$)RJ$qul9JMTb#1iDi9p^Xo^z$lthD3==ufNKjJaK1N<z(}? z>dp}9Y1OuI|2}yyS(&u)Iy28oub1+?H>_ofe(u>)>Y#Oc6^G*#m1(aRmInTrCVY0< z^yxcmUz=%hm+o52xA<ZG3=2;VXZcoM!zyp>HS5;e7WK>jJ=Xv6r2ikWwF(IqGM7Ht zoaosWD3r*uHjE)^<L=FeFJ2T}`2O;#tRlC=oL);k4jh&W%oUN)QFoPIl;NYr6L(|L zRX#xnA>R*6+x1nPq|e3fi8v7u<*#1nzJI<^S!VFo+Hj$$smros7k}M#zW(~$^1QNR zt(ODElR_AcN>APME#MXBwp^bXB+Rz@^l7cmUdx^PjvXxUm=(3Ov{}<;V#v}jV!Pht z=YMMT|30%vS!k&M)2+8=mxLEMa5rB()>O}=wpdWzf9^y6zZ319Y(iiC<G85q=^-x0 zW%lCWK}Ijlu&^*WLGDwlLytc;^wO+9WT3<KNH1o`fvc;-f4<!RzuriSC(^!fQiV<0 z=YO_VZ~5Mau`ku#T3wScVOR4PqYL3x+t@ggyP2k{bv;(n7H&&CxN=AJb2IDRyCo7M z;@%W)sqgR;<>t;+Tj_L^^SNH!qcb~;bMo?xX8PQ?apS?u<@2B2y2-tv`hY@^PMNp| zPrXZE%G9^TydvkHuQk8F<o<p2z|3cJTbF!PbZVY(iD^}wT5ZB6t+csHJqMj1{g!Lw z_#nWM%W;UopJPj-cli3El9G@YOV4CWT$?g2V47;?t>Y{GGIr(&uRHZ<N75X-<25@g zq(XPsU;X$j)GBOa=IkZ8tFr`VnfYDw+PHr^`%1O5^{Gr-4|1I5zj!MxValbJ#E!0H zzINtCDvp`Ht8y03y);c^X|K`;rkeAI6K>qR=UQQ=)3KQ2{`WVF&K_6(5P9=f+Vh#| z7QB02$G&&sIQZ$4(eK~CTO%%Hm^^y%qNBN)`PbjNK;=yvHy+$s{Cr2vPowqwziH*~ z`DS$Kb3}aP!?ou3>KEM2Q%;$YC?ogLV_~1l>b0&07v62@c-(TZpkoS?qDr8Qu@v9^ zbuDIXQ>Tjl`u+RP{`&sC{}|`yT@SkRSR%e^0T0uKTLnLFg>JcKQ}bhjMW3M4hJD+P zC8aZ-=U5_s>7}m1qLz;tbC#Z8`|#}+tC;%FzH)haMJ5s(w`}P!Y&%&|-z0GD#X`}^ zD$9i1%(PWhqdc7K6$}le`Xi&F774d4+1D>1bpF7lV{GF3@y`w(W@nX~ls(bpE%)0^ zme+GHtz$8pZW``(s-#!rPvt_F;Gc(&KaDXF%qrY^C6vW1JaF2s=|*jb>*lvAq}a$M zZ`rivtBLLoVGhSxT2s8%ZrEse^nZQJ1c_wtKfZpEqH><AX1E9+3Y+~oeBA^U&VBL* zUP66+-#9mAe+|3-A$%IAQQFBzkCfzp9AMv3^pxvw-ToJMckkbS{@gh}etEkm2b<Y@ zjwc^7cyeZ@am=2IiP_iJedMqI^gqEyW@F(ik;Mla{_c$p3%h1%Bc-%NEu*!k-$eJ} z#d@w9pQNv*F3%db95TD8q9*Gv!OJ$sqO>b+eYnKd+2;2){qPbg&U!3zt07GDP_u=n zZg+^bUfkYO_wLF4t($M+o6CK9tErk>p~^yilkR!#B8E=4CkSW?96RNIZ(nKm+_|!k zEDlbV`>3MUDH^BtrKO=_(j_h?r~6Es3s-S838dDW1j;Dda?em-dG1iy!Cx~s&#|a% zx_-Z6?!}A{5B89+A2OSaudH0Kvn-+@c6n*8e$3ad`xh_lKIL_lQMdH${#fg^yEpxb zQTZ6VK}P(+R*?`{e~B;rk#R9k3JNBKzRn7n6=j&_uyk5x=7u?45t;|~^xjH7zFc(M zhn99D&B-o?GwXdKVj`Ygnz~wI?ajNlpBB&mk;}*bODAVeijb?$-fvRX5=XwiZkhkn z#QpZ|)(7VvJeXkO%egytk;Sshd4(_UM6w8^?{In7y65LJ={^5mg@34>zjdnUy^5cy zOp+7SzC|5YNGbfua(&(0huV+NN87av$?&qT@a!^R(zID3pfZ_ras9&ydE33$UqAfW zCM-Ps=b7c(9c~KfXKWQX)s|<)?ap*Ry)t)d_jJ9&ii#5%CR$UDJW^3K(zN;@aYT7h zij%kRdG5x*HWS|))dl=pG>?^<$Se+G<xmlty6lK&)2o!MtY9|VDNOO6PWCFY?mleF zEKHI=bk?qIG;HO#wCCS(U$goxMthccrfukIe|qV|YOPHxj4S#(giL)4cO{AVRyT!x z^nWE$UaFTgbJCT<3dPIo4<BZBYgV3C#jP>5AuQp{?ab)pH+kM+Cd;;}rk#9Kwdum~ zH18b7D4E+zCW}oIw;!HqynN!m^WOXaTh@J-|7&vk`MF6kJ5Sxar>A@Wx%tK#{`#Lw z^(SfgPP#hdh-Xij*qqpm?U4~L5<dL6U|zpjcO!pO)7)Q^|Np+9Y$R3m>PqI7ltUE; zF3O&lX84^xo^iNXf6x5ydwy5u)qL7~!a`L*=<QFJmn@AbHd2eXI&mLNvC-`D5Ma^K z)&2P3UhO0k#ShF;i2+Lv^=`@u(Eku&S2?M;UR<x}_qp6h7J4o%W##3PJnd$mQ%#&I zJeZX}-|I2s;B=G_kUz#!=kQ9kWvzfe_m*u%tn()LTxy9}dPGY%;vf^FlMqL<zRJAk z$&Z#?*e)7(;H2?#$uuRm$tRb*cyH`K;lxqx0?u+d_Vw98CE?4oU!QXSw7TojDVN^a zCOcMTG1o^dWsJOL{=_2obnM1iZi*6Ziw`r`B+lq@e_`U9mGeq)HS3}+hc<t{a!M|% zSN-i0bGOBtdHmElJVFnt%u5OC;JVLmQTj|~u3vlMtWBS<&3<v%xv6|__uIwJZOx2Y zJc4WaHeRd9^qQlg_SiFQdvf`YlNmf(N13mkpZC}{{<CRM{qf|ewPK)2_wV-Kt80F8 z&sPqeHFasKS^hmIF3&YVHRqMTFdtj_X!gFZ(VlJ2ds1Ayowq-ItowC_Ot0b1b^Qm! zl(J(5&P1&h`~Ca(oxR1g_x@w7d$IXibW)AMhSu^^r({={q-Lg0ZN2Bj81-z+PwDIH zH}Bo*`v1X%u+?Wj9b&4#_~U?`eX@hs5|JNo#a3QClG*d6cA8j~51S#A&fU9{T{2rv z&De1)<MqM{JI4nLCr#NVpD^VsOY8H8t%{rlPiHRY=GQ!b=FI9ut;63|8SGI1Srn+X zCOLO{YTl_n_0xHKR(;vt$MrhO@BONsQO6xE{f^sB&w99O`n6gA?&)<WcAM0933w^@ z95ZH{DcbItYp^oOM#@m?*2(3QS}J4=rT9FBThmn*dmed^;b7D`(?#&2${~Iyz1ZDs z$08iQyp!FNC%$o8-vqh0e(ekHE_+?cVzpKE^Sr6j6C!7GZrRxF&Y||$@cjM{c6xDp zPJl*%3l4tLtNU$U-#qtiZq;7bMF%I;-<=z4(zfmW<MiZ`e=C<)-sjd=n4<JWQGj!w z!4gUBawDn5Q#T!0-(V6Pb^3McySqCNpSz|OIA_C#3yZgJaF}(()px=dm8eX=`PRn^ zEOZ#ImhJXkfBo^fx6x%h$CIZ`pBC{(XThZ-X|0=hyf3_(u!Pgyy<KJImIe{IOc~)e zxyJnZnKq9t&TaCXkS4l!ZCJMg&&8E$U!0s213kMOA5_~p?mqlfJKVy?CdEQVEOPzJ zQ^nJ_8unX14N`ivYvQ!rU%m$X;yhnuL)z6jr|h~WaAoOix$nzj7}g$7<mXcCSlsdO zpQ2NCiIC%@2a?VD4$Hb692j`|-M*-EPW&LDeNdyVvtCRi>POt{fR-073MJg<w{ZA& z`&~%POr6R-U2mb`m+xgS^K*subQO2!+Gs|+C^25ya6{~srnDBPW{$sfGNXTdRm;5o zFDlpPRsIqzx+%W@1FPNdm;M{e4W1nN#Kq1puQX+mbXx^m%OwSye>c9@@H<Lux{$f~ z!dr#QET*#zWcunojlP;r)hfBDZm!vIPl4mcty@d>@9)1j>z<SN++|A&lZ@5&xxcvG zbGh~ObZ*O|b9`q@D=yY;Ps_{{oUGz<m}8S&l91#2>&%K0eC!K&*BYpNsO<^+F|S=; zmCbim=*3&ge!(`H95vDFQ+Qe~9Tt~k)0RsWSTo1w;8N{yuH37c^~y$TGvc?c+@<ql zm&+lWM@zU@uE|`sHEZs)n{4}Yp4;m&KC_UVb8|~>=nO;e()G#<uQIwGf4xK9Qi{hQ zv!_qVXOcjLP@`gRu+8F+6Q`Np%=>)z{@=S>4oxeq_pRgku;j#*JEd;(HdQYXV0u}- zX5E?vCOP-^9sT!g*-@RAEl<=n>pQNrxMbgY%jcNY8P_<WBS0u^UWfVy``>ordT}~s z<x556|8I?d`0T88%+9i@2A((G%3t7hR;jFJIlD9a`VZ##|Lg|G4E%!s#7)0(`}k?A z-<9+FFL7z=JQMiExcK7^g~yC<-oBfpV(2;RQr1>awL-N|${$<WO=g`->R*2UYffW* z>ywf{XWRLA*1zX_e3HfHU{U_W3Iioip_CWRQyPxtZ(ZZFWP_5wSLUL^7Ox;Cf1#>L zg(*{?*}N1y&UyIcjH`af+ooF`<X82|?ECiXn!+zeCl9gd(=xW-Ze37(&VOdQ%`5@+ z=cy@u&Gj<vs{+K&nF~G5D!N$eTX5>r+0V0nh18o0tz&Lo#r1lY*`BOJl_%rpslR;x zUU90EzuJ#u@7gkDl&5Z6qPA3E<+9!T&TT$#n{I4Yx$)MQinDE#XBuu?tGedW&$Hq2 z>Hi;eY(8&y{Ncrh44Z=zzBZj6HuC2)qK>8PUdhV-cUr!BmCQqF=k&iU>i+X|PTjEo zyVC#T$^Rd23)HjidHpH%Qae8<D@S~k4O8PJA%CvAzt`(TnPci7^M1Rf%O;(@WxDu; zjZ5cM&uf=w;dnVQO=ZKMsVbc1dH1(Dd0pBht=_D-Z^Oc-tA10r?B<TCeWeMSPuzZc z>E)LN&(28JeZK#fjlJ61rSQzj-pOB54*dJZyrd#pD2V%DYj$}3jkoU;gJyGbFKO=! zF3IYhdZs`}vLZwAqMKotlFzCS%+9m4Y8WMYyRIrL{`~YytNG!FiD%BN*))fH=WC15 z$Dge}PhC3sbjY*Vs1v(Rm7cL)X73-i-fR8g(9l}B;I9@&oq8sf;zqNz54>7dzVgNU zcS=(ZEzwM|O%9mWpAgb@;$HpkqO9hV87@yQIQGBv7MV6n&fhKL<;IWm#3T0AND8WN zv5sBMBmbi$|BjlSl#tDTxl0c(7`bn(TAI14Lp|AsIsSLAyzZ5&k1U>r|GTPR{QjWx zyt-$R*Lo83O%^@=d8Du_q|2c~LG|zL>p!p7*G<k45?<cbdGUnb^ouG=Qa%Pw^-+5q zy`~6y@M_gnH|FN%7JWVz-J>qpy;pAa>C-B|**%gcaJ;_gaq`hCEpBe^5-Ztj*RDmx z#=3f))Q<nvbX0xGgHt<;J1%xyyeJqa(Ii;maqZlkXM6Sc&7c1}{r`+Yk;`1pVbjAt z1R2Z>yzc2$zVi62bj~KHiU*#HOcuUnH9J$!$iz5LKX7H+ni(H1UG^8R@QB+xd%EzH zRL<1~+m}h6UiHRPcIFzc+ciPDd9~A9Z>n?$PP<yj+n;8C|4UjdhiU4Us_Dg^nv#6( zK79Niw<>ljha@!yykuYE$+Gh7p`OW8vZw3ayz6Xr#p)c-#2_wzk9`mM>w3P|zi=*z z(w$v-vR*8q=$Xm+|IPIWvghi>e_mWaGjRX>nF8J{r_24828CU_CfR>nM^Eq5%lm&t z=Kp%~UZGj+{5(tVKLwLb`V2g{Ii$O%RWcp9u*o)5WN~ooa@J$hd)%)r{r*SbVI+?u z$80U%N!y=Fo6Fm!)R&7VKR9wrO8r{cO<Nn4i);%+10$>JKdxNOFvp^>>FSvUf1c(4 zu`<73(R}u;Y>oYr?vx!{yG1s6yiMW>pI1F?>C;rXd-V+>P4^x#adXGE-EN=B*L`5g zEdH9m*Ne;3C;$Iq{a;Sew&uLLA=l)JFxz@BNiBYJ&YE?0f6rLIc(-}xOszX>eSR(C z+!1-KbXKhXI^IXGKlInDSsng2`GEDazc&7!uRR&8LO1=^oBQZ_X+%ELr>c4G{}=S9 zn^YMob{?yU>}|VdmSQLo(AFt1$GUu7*trc;1Xqj9dtdzYmg$}Y?&+nLm7LWz?_|WU zUB4L{|NKg)aKR&%?Rj?}-P>FJ=IvX<nLeA$cw%o&=8Vc-r19wf-`n-4-_`%K^WXnE zc1`k*{|^{fic~muv^ypW+;Ujuv9$f1M<mnCrOBcSAOB2CZDcyMS#(q5^P3me8Ys<P zQrPsqQb%9cF~xabnf8_+Ee#>kdNF(1ZbT_H{Qmy_bN2o}r|;~mo_=>RqjdIamFVgt z&4&BlFYY-wok{h<?ftK<cNRXbS|%&Bx&Ff22i0<gBI%-s<eVhzKApXPL_GeN(EOiw z|F@j;s4vV4e&rN(Yi}UiV@`o<0l)8>O<y1P^xNFq{VhhRXBWh<bk4d~7<%@4W!%5Q zf_+*s8_${jSf@9Y+wann#4C?YSjuHNrpJ6QWXb;hx3)w0(M5)(rgGa9g|yB(SoN}r znfYC;e_1AP{apU2p^~?+Uwo#J>zhis^C?qzuHW}dOMm|#BlG-wOYXm4Y`pK-%gf7O zey!SaJGbt8x&6`lUr*FIciYa}Uor9H+-ja<%D48qzVQlT*_n8uD<XS|V9QFzrGoqX zAGEuyRP^F8+Hxu<%<$fF(TD`IdfOfK=cSBU=9YDz{HIv&DI>h)_6iF(iDn+=xIYir z@BKT|{2M&0x9`)X={GEG7F>V3Axq`e2M);_QDTj`i$7kuzV`&X-492*f9vb7c3Efk z&VQ(=7098oPe6bzruvm8A6xshS8_*ZI0#AS2JbzVa`$^qNG6K}=iPfs(#y>%eomdv zZMf*>r~2@ygEn0CuCgfx=L4>ER@wT^2uc;m-fY}2yg8INNmd~GsqKzk)$8U6iF@T= zW$@PYoVslP?!!L{8ahRlIC5=$+B!4Va<cMlO?dq1=<!>kUD>BHK5yR8d2N;l$2B9X zZ@tpy6>qm*-%<FO&E4Jo$G-Ewep#J9eY*AC%ltpH>knTnI5L%cdPKdC$aZsc!$j`K ztP+_kA2y0dU7C~MyjX=*Qk6sb!;*viHd8`;grZYku!sE)QJCOlB*oXAy{G!OndhR3 zm()e%PNuZ@iJaP8TPd=k=9kEG{TKyLp}6|LrvKj0|8M#4@%um3Zj(9Z8a(mY?r!eY zvEhi(LQ&cMM=dyhy#IIP|A7R9`UhvP-&gSsn()B8|D!^Zkm`?l2P86dOCs$vuimzr z%j%~5dZs4h#%n)LE!beuaZTxP1<&c%ua{&-wX}UcH1qL_O}p1^*V^;m;*pF0qQ|K# z1q)T<O%H0l(sP)WnIhACHT-+QsvFza>1(RGA67j4&o0ZxiKAIi$iK?Nf6_6|j}=b! z0>9q)2d$fH)jNf0?K~Uf!f7uiZ!fR;@PLtD?uOUp*4EZV*WbQ+r8WQmpZA|{onC+8 zRmqOBZzjKI&z3p5ap|3OHZPC<iBmYvtADkbVs=4jMp;K-*TV!CA>Ru^HT!c_S!}CC zGZz^&zO`izQ+xPu?{AG%j%Y>3Nhd9|7s}r(S$)5rOVIJe>G)ql@pUg%>mFaXKX-O} z+;NGsPB)S~f9XhgYO#1#g(=+P50Hravn~G-v-}?g{k>mOx0P>WSbaf`O*dpoPQ+4H z&8DlW+C3$2Z<+e#%vo~(ee|0X8OOAiPIj2JmTfK1z7HK+(;nZ6<m2b>{_;-e?%zU@ ztFJ$}O}D)JM5_K&)yG|KY)c>dOlGtAtdlJIDLOSLW|ipaONUb5>|UjFaq^3QPb(+w zZMM6sk~l-HXR<^~?xDBra&^mlPi0)`?2L?ye0Y*eVcPy<j|~scG<ovqk<#6~?HL|l z*Z-RzpJX9Z@a+m)?1Q}h{hZNDf}#{ePda$*`^sM5u=VQIMMs_VcGTDWe&$>EZ~oul zynUr}Tl3nxxx0T<sCae>is@fEWgr>N_WIg2HFtOBEoLQAvP&8(goOQtRxf<SJ<0RP z-r43qSML8VY*X=JLUS`SOV_mL!cv@phdi{j*Cz11-QcQWduFEap9k~*2%9l~DG;7` zRkw0p|9j82$%1OW2K)Ty99c83K8F3llasuDb8kJk7Ja?*@)Q+62~E!go1k2WiZJG5 z5eFA*`^Rd(cz4;<_%p-VRX;6u^DX<SczO2A!rvE7zFv!6!hWjY(9QinkH-dl_Ing$ z7jxBdeICQ?M;fL4J2EFunP|7{etNRWBi+TF0vt;_FB$4yR^xEWXmwec_Vtcrm3Mc& zG2@Bw_oj1;&Nxq>I#twfzTMfS-qS1IUJbAOJT;snN>^{LrEh)p?8O#}9MRh%r#&}z zEc#Zf|KWc9|L@FUYyuiyzuw>7eR}=h+40FgZfWbZdY9Z*oSf)1Nh{?+qgjuu+YzI- z-R18LY@{Y>9L>7nbd=L`Q2@v5hZ#B57r!)EE7#v%bm_WO<CX<_t1_zIWjbqfa5hh9 zRR0nG?^67eOG{5P+}TyhEPvXcGfMFi$D)UV60PbsK}#!M?JO5f-ny}n>!s(U!mf!A z9$)^-WWu(T<<jXJ`w|aK&A$HP`|nnjB$?y=6VAkNN5qOH2ghA2yyhnS$~pGN)cE=L zTihQ?T?;AvR=--xeJ9hJ)P>Wv<$_B6-!CwHK2u&(l3D$G*^N7QQx$4ko-g+0Iizmb zWZ{?j({A@)yYu@##NCpV{$zh#d;hmRq0SO*b8IRnaf|DL7P<V}KL5w=9ydX0bw44g z9Ul{fAOBu5?}$XBJ~Q8?De}Mey-zoq`Qehc{>Gg<JEu&#yxhP3^Sko>(lZO4YR+%( zJu%g3waEG7-xddQ2Bvft6^ax-{#E+OLeX!_*;_tJNmG>$9VpUO<9h2kCD6sQ>(a`r zQ_o7CbWJ;2W$EPgZOOy-f2aCCP0at}#^82X@%sIWW)r_PUlu)Ae8eK_?s5K`@wY=0 z((nC^F$@czUnQled*5BV!@1s3X(OBZ)opUeo0mS7^FFP;=hv;(GP1Ha;||OeocXTM zWcJiGlfCEgw#GDEj9hf{jpZwww~q^!pE_{o=GW^Q)|Z!T^o`VdwZmnTmh#hCm-UP* zE}Q(i)3GGe>-EHdW9jb`<TqB%U+Z~fiL?M$^GBQ0S6midV$o#z>v`nTt66Wag<Y!u z^lA0O&L@+1z&BukR%2-E?>+K0JkFWp<jbw5+uq%KsL}P}TU7#!(U*&H2R6t5u8XU9 zU;XpSa=W9`_y0-Vwrv|nQ^V%;^GpR<$<2Pdb6b0l3s16e%j*5`*6F%i;uN(`HJ&!- zy!*RcXQ^={DtJopt1r2c<F_V<aZbI!p>GwQK|KZ>Ci~VKvzp)Dzw9!XPT{)SkM#e% zh=1lR|4ZQeySvIf&X-gk9y}|pGMTecq-CDFiA?0<XP!$g{@_+jKR;jl*el!f``cSv zS^4_gUYNK_`z|qE_<ZqF2gwhu$7ga+m*{JAJ%2YQs%T!@i9*&{t2JJi=Bl4^>6KGo zR9|p1NzPlYa9&7a@Ux8A9}kqa#FRcxt$e%d*@?^dbK5Obe;-xhJoH^K^4FEmw#Rnw zzH{qlVsyoekj1PLHPOG7L^K6dIoDoI6$_sxT^)1Wi&^|tW@cmJmMC2@F|mlKsHD=; zsqsIj?{_)=Ju)K7WjQDR_Oc0%9;<lwJr~>()pNOiiv1t={muHdFTHD?YR5mk8XljT zo!wow`)r2EqU*13e)<<D?tja#bOC2gbnBX<N6vBQ9?&S;w%zg-XeRTP?)+)b4d)zL z66Kt=*(mDNnI8=k#g0s^c)%)=Y3eL;LB6-*{`Gp<`=9sTKmF>K)cl`k(r5UneW|i_ zYi4X-tLwzGw|>FVU!RQb860vK=7@G_c)sz%>yOu>^A}D@dVEFGYVNww4C7N}_b%Uh zwWC6=?#0b%B6H1V&z_x=n|ty7clki$>{+^Nv_2O+oPFixO1bS5w&Y#bV@|%G=rWHb ze{tu&%&C)RcC$a!S{<L}@Jw>&orWWYJ|eDR{)?_V^R-JRJU6tlseiEbuU#WYuIj4i ziwz|v_RgOo!`C)vPu0w=KAf*tPoFliL&ecgZNt8Q9hbu1^=`SH8~5wd^c|mXy>6Lz z{MIL-?Rj?;gV^*l7MduqF8r|b*SmC!;%7SZf8Tjt@p|p{9i^{9!;;`-rO)5p-MvOf zfAN+@ck?D5(M)`BIs3PU=*0Ro+sq}`T-@FNynFwLuR1!KgJ-q3t=O$xzDpvxIX5o6 z`x(~2sr~K^tDIZ(dV{l3{&Q|voZOne?&DVZ9}MxI4{^)L$$fh1Uq9>a-Pj&O9ew@9 zlV)6dB_+#}W#RJ6ZevHlT!xy}=f2%3K40|yo^4rKS%BBZJ$ue<|MM{a`NO}zW$IHS z#ivH_il5)U)^v*BO^fPTv(58AeOSm^v)oe2_iN2_$;M2FtvafIcC;OOy7{=Kc;=f< zlbssZFQm>3e>h3AXeGzIud`z0HUxF4cy=u6_*ii!!N6h7`p5aXiuXUuT{1~6PjCO1 zaHPomfQNII$f*=3XQvBX%zbXj^Wvh@6BoX&SD9P>=HqvpCkYMPx3a#kuU~lBu=ZSr z(L?V2pFP8F$6BrK?s+_A?c3Tt3s{{dWpNxfb3e>@{|CGMv9;0LcT|2>n>~9r>x>m! zpMCf+VeejBv+dh-&dRQKwDJqKSuC+kaAnYoE7yche(Z43JQug?#NoHOdlV9u1ijdr zd2(Iqv0szs)lc!bfA}-gAA<$EFQ{@jTixZGt{?yK(Npg^HkCpzD`WQ8&6PIK+pu-( z(Tf)agS<fd>LMc_GRyy#*!zxYZeF|J@@7AwM=FZGqPwfBS>Bp~`V7_8)vm^CjnmJA z7LZ?xj?t<5%)vO>(`Ent^z7`(&CSm1;`g6>_AITj^USAu=T$#EOJg}-Ih=i*k{Dyd z6aH#%_Kc_3D~0-Rzgn;N^wlKpdZFuP+VTeZ3nr^18qF*yDLL}*pPkKjnTM;_PpUaC zpZNb^?YsNC{PbTe=;6O{&qyn{WcsD!5}f;vpZX;=@A>UTd)7}A<aFM1H#TbaY@c<p zk3U?~WoveHJl*$r#@)O1u^&{W=0|Y7d;8{6Z2Th8wy=N`4%#0o?-##czJ13D>HIx` zZ+7i03ZJ#C=Xmn3zjnKK@3#5<?)lDNUoLNZ>nd_FtF<Irx90rjQ{PrD+P&pOyWJn& zmc$d<`uopR*tl^q9!m07dYvuZ;;6`Rg)6#5T#Vze!1>QJeAM2&dsp=TAoIKW`}@!D zt^UsSSj<(>QNTc|*W>b(&(F_q-m&Y{$;s-GQBjA!*Q!nKeA>jq$S-H3!P9uI>d~4t zYeZ_RrcIxox&3y=T1mck85x-k`|m%$x3~Jv?(+V-ckep&WZdcqDGgY&XnA<+jn7UU z%WN}FxtyKP@v-on)T*n(D_66WFzTIlIHOhX{P5D@V^gmiNjsn0t-dyw|K`OEP()Q% zOYeI*`^Deh{f=Mk|7mX9X14R=vC=CBs)E96jOK5YRZ#s==OA-*P2QTqcYi8(XY8A` zUSYxCsp~How%yj&{<8na(M2041)cijdMtU}zTbD(tX=o$r``LX2?w0=8SPJ(RLnb` zV34rz`r~?#qjwg!|NZ+{>D&3^hJDF={QNg>-Fo!t>1mtKZ;I#G)z13*wNy+@Y>h{- zjChpv<Bv0%oumJhJ-f4W5p(>#R{igPir#3NSKYpSyH(smf(JBKUS7Wa$0^Y_Q#Z9% zS6c47Hf!->Wixa0?zYH68Dj|^@VfW+;@_v1KbPjIX9ES^=FP!hjt9=vc~oGr<o^53 zn>PnbOsI_WxVk6DDZb#O&aO7a(qC%6#<TS9b-r)PpAvcNF^AQ8=GK_sc{?5`ALr~} zCVo1>AR#}$|M&Oz#lMy19|o;%{a!u)=<c$q-Ou%lEaoWeVbEmN(oVkaCNBGYo!w^< zb-y_k?Fnav>(4x0<a}E0?cZPPmH%Cke{}Gh+3(-KSyvl{v)#UbKV32V#@=0f&b)f1 zbvI93QPp#ffrQM$DUZ(0wFb?$-@SV`sa!nL)+g`F@yCX@XU_Wb@1LFa>C-x?QmY&U zxzsY7%mmVRmkG4B?Cbxs$Em{7T*)i*l8NS?c^1ZMfg+-AnN#XJm7o93FWz+hwq|Z; zSl8iZW-&3bO&eT9;+|&M=yg<7ez!mWnJc-xqQF6*O^iLT%c83@>Gta94<0<2_)Vbn zZ{)J)WqrOoB^q{3$%$OL&im-^J@c+^ey(x)RY+&G@BcUEjQ-Cb%KCbLdwnl@so48l z9miFlT35@uu5Q&-{&ehcdwtBW?)r)M?0>n>`F*>7_WNI#>!o(?c8>Ydaq^sI(bK|! zQ&P(3zt8XZZISKZBd{Pm>_y$D$!qK?4m^zi|N74VGwEHj#`$k&?0$Mo!tJpk`(I`; zv0Y(C0@0n)u7Zazo=B4WYj^nK1+6wuPXU&r-}kUSFDP)4ap!4gc4OCmvpq_oe(BRz zm5odPt`-seaU?Khb>Gq@w=*B7H+cqCO_N9$$z0`eX^P3#AFh*BmS~+d@?G`gX45gQ z$-ZUw($R`6nIgs^>^<$y#}+Ftl#e*qvF-TmBZiq}e|M@+Ow%fue6q!WgQHnMbB$Kb zq^R>6U5njQ1fCb#r-?7}Hd~w{y!P}b8@>99SFcRZ3frv;lBsdOKXX}PD>wgQt=`b5 zzEh_9?&jD#C9vl7g-?H~<<5z?J?S;K_`R|J<DHwE=loj!f68C`&*>gv;hc*k9!YH2 z)E0S}QGiJ|=vcF(<F6{E%os(3pr+@FEek%sS{?Ie<@}^S&h|%M^UD|g|5y9%`*-=y z+rA6Sv+Bhk*6sISetDxr%M=fvZLg216#5uR8A|r9$yvDN?AFk%=s>TQ(8$>ly5aXf z-|#g}6u9NIRmUXIOYo>5*Y1q92`XEn6uIASTg!CrSbV1IDv`jP%lj-h%w5)*yL#g5 zhKyeRil9e7!)!DHwSp_QuSz==QJ7)OcG>L3m9}M3nHM7KC$Bm4BV+A>i1jBv+i3SJ z>D6_*q?Ns#^HRjoe~}MAYOUXMhI`)i+bcZ(de3!#J86~fXQvqL($(kQ%jY|rEM2QG ztHm|dZLwBa_N||JX1!slty6<oJAOPk$h<Cof8N3E_FtFZ_@Q0TTKDGojX&%ENqoGu zOEg2wI4SkkvdX_@vkPa{E8WVyt?;s9+T*vsuSsojJ7T!^d~IKE{lD{)HHXbNe4qFI z!?or1XPepiyF{LBX(+g-oN6F(;oLgcwBAKlYX$f{9;*mPZJYABPlUxWf%~4#{cmfg z3a$M7dV|%b=9G>p)1()LbOn_?^jpNaGVExc*blkI6OS*syik^9n#iPwo>AHL$0l<Y zx=lX6S7Y+a^{3Vt-8f&z*LvV`*m2X)Q>!-q-uG5XLnTW3k%{Vu+LldCEb~5a7)+a< zWWtogFLhq|LydOQTvI9Sc}opvnsxar1+Vrr`YQ9fA#ghHCeGczEM9NQ{8f4HSHaSg z)j_BJU!9s8rTQfJ`0E+HAD^XW#T;o{^(&?RjCI-6uk)^pr_4LP<W}yreV?r@KX&?W z_^`f?RkrO#Tz}aOr)8XVvxGES*9ZLx5zOeFt};VoneGAaN$&gps2ubT`eSl*_UEU| zyA*ss_U0B&$jiT2{nk{h?rZqVzjw|X{^)z3@VWnA;Jz<Y*MB&5TEEy{Sn<@y346Zx z8nmhOtzA+d|IfB-(>HEmHP4tWe|&;(OK>HgesFTel0)AGZ!Ve~bjxtkwXgo6T|q0C zZcBgnUuBlajJDKrt)7j0yH2u9y|hL<VN#|3WDPZ4n~8f^H5)dBUO3hzS2=I5*za=f zu-y4G|5{$3;o8w;_5Z{n*57uS)gRg-dnfYxZd_HsRB~-$KzjY(Nu}Ek`mEkPeeKj$ zzFNmU<d%Qg=hv`0i@7|~z4Xbs%GamYGZn;0REGZfwQI)u&RxNila@SwcI`8d@4Ma~ zM^!fOTJrw)=K5*N?^iT`mj7?j9REwzXZ2Ag1F7De%`5J(9ycqxq!#QVKIy>sO?H}6 zJx{0JxVM*0+2x=OYooej&+&SddFQispSAEcNbLTw_kHU7{J*+oRkrtPzKY(f_{;n5 z?q2Wudi&FIDLJQBPH;?DbP;jgrl-o?dC1_%>>E)t@|O2L2z**N*R(b0rfo(JH|Ij$ zsHoaYGRYejWaMnt33OSM6WcpQXlaepT-oyu%P(*AJ>Q#KJyV5K!up-(v0Z1K>kW4& ztYy6H*~Gb~JYmbpEh(vC_g7quN@&@3VDq(^Zx)@K@@2E)Njvt4_lF&3wdz={7E=4Y zd*j-VA6j%=rgJI3x{!43>HE|w`BavzR%SmQy9kF)bd;0}k(j9VX19^{FALtiv1^Nj zkIg-8c$z0#ZZ<z#bJFt*QO0upH}Bqk`fO`<z0KF-|1Fj;?x>qpbm8^Iyw-gEJvqlu zr=IwfSKfam#%JZ8?l)^wgw5w0Y&6qR;SArzTB!Y@Q>^Mu#@Vye<DY$78=dn$XY=mu z#~s728+NX_;J+`rYM=kCWk=V|Ny&`XT)SxZQP!lOsX}au^U}prC$^qg7n<1{#+Avk z@A<-1o>N)%bDr%keph?M^N@<L(YeD7CY~8vqmFDmyJpL#l2zAkeLH%#W!>Tvldkm` z{<yjI_rCu3x1R+UPI1U!_x3ftbZuwg$sHP%QsD|ln#WgG3xz$=G@U84=;G1hj^NuY zJ!{ImUhg>@`tw|kjPHcXnbzVqPb@rp)UsE2&Y!h2>wW*`)#ifr)$2YR&62y`YWw0) z&D?)Ed~VO4s4Nv(e6i#2-`aQg_n&{g+|O1?<IcIN+kWZ1Y3FDA%S>lG_w%=V=Bd3p z$t4vPC+_Y2os*xxdF$4r@7}F@+cSOQ`eniMf9~AF^I?uT<E1ZeBjPoWM)<ASlX&A) z0pDxiig`L#S0$M@PGUKxwRrZK`U4tMC#h*p*3i1NrE=DdZ<5<2+$7khNngqmm2-ag z_vwS2IWMQ*Dz0u-yx6GsL44-hzSf85`;?86HCxxd{aAB<)uGg|T|2&?v9R0Zu=(+m zdu?BPjNVSjDW1Xm`gKJ}|L(dN@qO8+C#&66n*BiOU*Vn>qi3;mt)gN%>x8vm97>&8 zKYjUp-J^QS%bvEW#QRusW>r_ue)=?3Onhxn*y^i`F6Tv>+xB0*U#NRaYr*uA#`(|f z#0)EsNu2#D#LmtR+PloM;^nDJH{CpTGR_I#G*AEBorEVhyEHmDnWFphD`m<KK7Ff} z)A4D=v5Y*HMViW!;=Si>J-Oc5%W;<4H3N;m-|GKQ2~|4%y8cu81nEU8ee#_8>6Vf^ z8Bffw-nYX>Q}@^>4sj=uhtI7mL?Tx#-u!E>P4|@k{LscZnuaF6yoVlFJPR$}#S?9* z=V4<$XXWOk<BW;3p6r`lAr$(*{N?4q@;^0<&v#eHO_?sf^y$=1Ys0z?cwTO~?drAN zBK;W`x684{`V%p)4~f>#Ua@TI(~ujocH)woy5e>&pD5k?Cu++z*2b(^4@~C1o^PhN zIdS%^L(E-gd<AZ#h1`6<S-2*Q|5$>w{mY(o;dGuWGQuZ&LloBww5fQe9WY8Z<BfQw z6Fj;8{LF)1H#dG*a%RKQ?Tmg?#pTj0x~6R6cxZDzDey8|*cplXh3?MY$->{7m|lcN z**@}|mdL6ny(nAh(9;>EDi^d;O{ag~vCj5>=|#WSp(dS=XGMfe4{P|WQzluxK~H2= z^@7WCj(+FQu}-i(zxZz6<KO$1mi(@p=vMp0BJ%mKq(mRx88HXks^YF4QkX0;N6Gx= z(dArxZgsjp_0}%i!Lxah>E?P>w$QVOmx<T#?R)mHPu4j2flZU-%KbSLpUa)t^4!oi zy=buvo7oz{hy-JP(IC^Otu~t`FN<8=^=0Cf@<f+sTb_JaX0!Z_;FM{X#lKIgj5(H) z>$`lDQD&L{=Hlf(8;&2{UAEGse63r((2`?DcHW^LpZNk8X<gXz&Mn5EYm-Ug)%wcU z9_ghX#ld2)_uNgbdgw2oY$GS$t;^1ESfS>M>A5}Jro5}39Y596G2J&NS!d;`eb+_0 zO+&SUWBt2bt#~I)YpvY;@`To>`?qfj#I{)eEZnQ}RD07hziE>fZr-*)Y<Yvb^Yojo zQ{vMfGM+6;+%(<mveFWbu>W(?S1)h4`lrUIUU^@r;E`qGKTBqq=T<!Ly}u<qD)iC^ z74ERrW-qi>RbBqDTxWKO>&_Y5SnF;o)!k9s|2anW`aY9T?mJ8OEj!7q!(F*5>GaF> zHOtT0HlOJ4j=k%b6S&0x^Ox+8H_s$^DCVT4pAt5l**+;*iz$$y<MQsKd)h8sK9+c^ z<IL*$Gmh!iryX8&Q)2%CNuvYB-)t^x-j94cqcZO8p6AxK>q3))Hs8DRhN=3C<cac@ zlF!GozwKMTadS=0vsTm4%vf=o4W|xFue=lb!*lYwsiC%;V&5OsPky*FDD!lW<J9yE zDqmI>9bT=ICcI(WnY-(S@0`7R_a&#bzH|`VGqK&bWbQ^z{W|Mpefh1IM~%4Irlqc4 zwS(jGb)(%YK0iKZe9Kk%?VG4&yqDKB&bB-MWvgGc*5>mCw=}}uuDX0a@s{!F-Wy-G zKL0=8?)aWJ4@)F0Ur+VBe|wMBOf8<pt1|Mf=ByDa&f8=-O;z&Mm!Q=jPHTx)@8^3{ zIcJ}~o@M@ZyS?jpclI~yU6(whRo|al7UbuD&n9H%S*A0_Zkzcl=FND&XU5i3%Co;R zoXS!^CDj{hJ3I10SykawPC@^6?#<878uny12dH`2)ULB>nOQ5By1OD$bshJ=3DTb* zrv$Is!*@OHww{xFYSbFvUmvEe?e*%tx~t&W(q~IwS)_gpn<}xkd-wMpY-{!QtYS*4 zPrq+2>+N67w^`}J=Ia)(V}0}|-DEY^pOYoT9{w@qndGEM|0O*?rszMq`TcyG!B3|- z&wbB+_Inn4-G=S?mxyyx6C?Qx%>K+i8u^Tq$+tQ#^!tsL{|~toO%^)rSTkSkbLzdQ z8RljCx~-p_V%C&Cu&L`-<+?k2I*rcC=w|=8yv3k?y_U~PBa76~1EIU$ud%k7%{c36 z{+YDH%gP_C<vKn$4PDRt>{)C1=RK1upFaA0EN7nLEYpzBd-mKs8rQz^%<SElkFOJw zC_j2R%}{mvhsAy3PqaD%&D#5e7hG?-9lG6bPL$h}g+Eg_h6lend{pWB)u%GsUxr?0 z4^T4-zUH5Pty`nM`{n6Fb1QE&c}F~+Ww-uQK+JAov%=0;vA~;;R(?)fedyw@3+qoQ zul{T`%j9_3#h00v^tZp;7i05Ft#hJLvhM7Z=vTkm%|w}sR3C*z{@5g~tDR95RBGiL zRb_T?+N(o{RwbJ~y)#8}^)8>#tCp*8X@;~Wg?_g=pD``btMucaEzj%2MT6}70`<7- zV|hYvRuvuOcrEgO6;EJnYyMT6Y3nBM(n~AyO_vSN-sH3PR;j@376ZNa`j@RTPetTR zYF)AGy_U|w*4r1BWL-`*+?iljnjH3hPVBKGPb)+AuIHMQH_L0SbbYx0W6zaOWz!eW zp8YekXwoK=#GC8h2x;$P*s(5iP5mzEyd;NL40h8?HmtUO#WiJDQ}O4d=T^V!BP&Yh z%>8fCb~p9&io=Slj%{2i9{AL8N1MoT%kACbk*#m9<T~kjPTpi0B&Otf-SdH0$;`U- zt3A9zZ{^OrG2!(STfM|}J{xzRYY3H;x_<KUth-r8k*Dp<z2{i*?R&KAbeQeyn_|m+ z>&pYL3+(h{e!I%xUh2iGlDC$XuuM<3^|Xzj&Y^t#{SxiA!Wi#&QhK4fXVm1pCl_Da zIkj~6w$mGD_s#7NTyxm{)SfoA19MJSge-j?YqQnp*{apy)pvGY-*VxzXUXE#uk;^h z#k?{Ly1sAMI=O_YcITCS&-7={jM2NIDwaFztfk(u`iHxWZ8)8uKH}TGp?db4MS6N` zUq8weTII8R!|XR5dPjCXmb$-8GhEzfxu?;BlUwE0uN2-r(r4WMJh!3r;i8;fKb}|e z9q%kX{phvYmg4KNSFbvlaI&vz*s-jzDzYLlEoYV3Ya2gLN$agye@?$E{Pypa#(m$- zGO2qeTJfLxS|578{!!rv>FGV*vTENf7)mGqwpsT)<k{B~$9tFWlj5B<OT%}!)V9iX z3Bk^R$8<Fg>z&%eJKZy4+4{)zme{h3M&Ucxt#t@p`t|XL&{Z#fq&|K#>)g8F@b!yz zO>WNVdpvV`==9xQ3TA;?*{=@nYFN*3>FVN)CS4oOfa6O`_OIOjMCpEg{G1%$$%Z+T z-s@{WUO!tS)l~WOx`jE<S52;5ar<mK@12|-kDUxkPO|KjSh1_?`m)3#+5Sa(!k1Jg tKR>iLtBHT-micS580_-z{`t?$z*AUZvgoN;9|HpegQu&X%Q~loCIEb%+DHHZ delta 8806 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 diff --git a/src/Jackett/Indexers/FrenchADN.cs b/src/Jackett/Indexers/FrenchADN.cs index 58238ce2..db57edf3 100644 --- a/src/Jackett/Indexers/FrenchADN.cs +++ b/src/Jackett/Indexers/FrenchADN.cs @@ -1,966 +1,987 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using CsQuery; -using Jackett.Models; -using Jackett.Models.IndexerConfig.Bespoke; -using Jackett.Services; -using Jackett.Utils; -using Jackett.Utils.Clients; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NLog; - -namespace Jackett.Indexers -{ - /// <summary> - /// Provider for French-ADN Private Tracker - /// </summary> - public class FrenchADN : BaseIndexer, IIndexer - { - private string LoginUrl { get { return SiteLink + "login.php?"; } } - private string LoginCheckUrl { get { return SiteLink + "takelogin.php"; } } - private string SearchUrl { get { return SiteLink + "browse.php"; } } - private string TorrentCommentUrl { get { return SiteLink + "details.php?id={id}#comments"; } } - private string TorrentDescriptionUrl { get { return SiteLink + "details.php?id={id}"; } } - private string TorrentDownloadUrl { get { return SiteLink + "download.php?id={id}"; } } - private string TorrentThanksUrl { get { return SiteLink + "takethanks.php"; } } - private bool Latency { get { return ConfigData.Latency.Value; } } - private bool DevMode { get { return ConfigData.DevMode.Value; } } - private bool CacheMode { get { return ConfigData.HardDriveCache.Value; } } - private string directory { get { return System.IO.Path.GetTempPath() + "Jackett\\" + MethodBase.GetCurrentMethod().DeclaringType.Name + "\\"; } } - - private Dictionary<string, string> emulatedBrowserHeaders = new Dictionary<string, string>(); - private CQ fDom = null; - - private ConfigurationDataFrenchADN ConfigData - { - get { return (ConfigurationDataFrenchADN)configData; } - set { base.configData = value; } - } - - public FrenchADN(IIndexerManagerService i, IWebClient w, Logger l, IProtectionService ps) - : base( - name: "French-ADN", - description: "Your French Family Provider", - link: "https://french-adn.com/", - caps: new TorznabCapabilities(), - manager: i, - client: w, - logger: l, - p: ps, - downloadBase: "https://french-adn.com/download.php?id=", - configData: new ConfigurationDataFrenchADN()) - { - // Clean capabilities - TorznabCaps.Categories.Clear(); - - // Movies - AddCategoryMapping("15", TorznabCatType.Movies); // ALL - AddCategoryMapping("108", TorznabCatType.MoviesSD); // TS CAM - AddCategoryMapping("25", TorznabCatType.MoviesSD); // BDRIP - AddCategoryMapping("56", TorznabCatType.MoviesSD); // BRRIP - AddCategoryMapping("16", TorznabCatType.MoviesSD); // DVDRIP - AddCategoryMapping("49", TorznabCatType.MoviesDVD); // TVRIP - AddCategoryMapping("102", TorznabCatType.MoviesWEBDL); // WEBRIP - AddCategoryMapping("105", TorznabCatType.MoviesHD); // 1080P - AddCategoryMapping("104", TorznabCatType.MoviesHD); // 720P - AddCategoryMapping("17", TorznabCatType.MoviesDVD); // DVD R - AddCategoryMapping("21", TorznabCatType.MoviesDVD); // DVD R5 - AddCategoryMapping("112", TorznabCatType.MoviesDVD); // DVD REMUX - AddCategoryMapping("107", TorznabCatType.Movies3D); // 3D - AddCategoryMapping("113", TorznabCatType.MoviesBluRay); // BLURAY - AddCategoryMapping("118", TorznabCatType.MoviesHD); // MHD - - // Series - AddCategoryMapping("41", TorznabCatType.TV); // ALL - AddCategoryMapping("43", TorznabCatType.TV); // VF - AddCategoryMapping("44", TorznabCatType.TV); // VOSTFR - AddCategoryMapping("42", TorznabCatType.TV); // PACK - - // TV - AddCategoryMapping("110", TorznabCatType.TV); // SHOWS - - // Anime - AddCategoryMapping("109", TorznabCatType.TVAnime); // ANIME - - // Manga - AddCategoryMapping("119", TorznabCatType.TVAnime); // MANGA - - // Documentaries - AddCategoryMapping("114", TorznabCatType.TVDocumentary); // DOCUMENTARY - - // Music - AddCategoryMapping("22", TorznabCatType.Audio); // ALL - AddCategoryMapping("24", TorznabCatType.AudioLossless); // FLAC - AddCategoryMapping("23", TorznabCatType.AudioMP3); // MP3 - - // Games - AddCategoryMapping("33", TorznabCatType.PCGames); // ALL - AddCategoryMapping("45", TorznabCatType.PCGames); // PC GAMES - AddCategoryMapping("93", TorznabCatType.Console3DS); // 3DS - AddCategoryMapping("94", TorznabCatType.Console); // PS2 - AddCategoryMapping("93", TorznabCatType.ConsolePS3); // PS3 - AddCategoryMapping("95", TorznabCatType.ConsolePSP); // PSP - AddCategoryMapping("35", TorznabCatType.ConsolePS3); // WII - - // Applications - AddCategoryMapping("11", TorznabCatType.PC); // ALL - AddCategoryMapping("12", TorznabCatType.PC); // APPS WINDOWS - AddCategoryMapping("97", TorznabCatType.PCMac); // APPS MAC - AddCategoryMapping("98", TorznabCatType.PC); // APPS LINUX - - // Books - AddCategoryMapping("115", TorznabCatType.BooksEbook); // EBOOK - AddCategoryMapping("114", TorznabCatType.BooksComics); // COMICS - - // Other - AddCategoryMapping("103", TorznabCatType.Other); // STAFF - } - - /// <summary> - /// Configure our FADN Provider - /// </summary> - /// <param name="configJson">Our params in Json</param> - /// <returns>Configuration state</returns> - public async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson) - { - // Retrieve config values set by Jackett's user - ConfigData.LoadValuesFromJson(configJson); - - // Check & Validate Config - validateConfig(); - - // Setting our data for a better emulated browser (maximum security) - // TODO: Encoded Content not supported by Jackett at this time - // emulatedBrowserHeaders.Add("Accept-Encoding", "gzip, deflate"); - - // If we want to simulate a browser - if (ConfigData.Browser.Value) - { - - // Clean headers - emulatedBrowserHeaders.Clear(); - - // Inject headers - emulatedBrowserHeaders.Add("Accept", ConfigData.HeaderAccept.Value); - emulatedBrowserHeaders.Add("Accept-Language", ConfigData.HeaderAcceptLang.Value); - emulatedBrowserHeaders.Add("DNT", Convert.ToInt32(ConfigData.HeaderDNT.Value).ToString()); - emulatedBrowserHeaders.Add("Upgrade-Insecure-Requests", Convert.ToInt32(ConfigData.HeaderUpgradeInsecure.Value).ToString()); - emulatedBrowserHeaders.Add("User-Agent", ConfigData.HeaderUserAgent.Value); - } - - // Build WebRequest for index - var myIndexRequest = new WebRequest() - { - Type = RequestType.GET, - Url = SiteLink, - Headers = emulatedBrowserHeaders - }; - - // Get index page for cookies - output("\nGetting index page (for cookies).. with " + SiteLink); - var indexPage = await webclient.GetString(myIndexRequest); - - // Building login form data - var pairs = new Dictionary<string, string> { - { "username", ConfigData.Username.Value }, - { "password", ConfigData.Password.Value } - }; - - // Build WebRequest for login - var myRequestLogin = new WebRequest() - { - Type = RequestType.GET, - Url = LoginUrl, - Headers = emulatedBrowserHeaders, - Cookies = indexPage.Cookies, - Referer = SiteLink - }; - - // Get login page -- (not used, but simulation needed by tracker security's checks) - latencyNow(); - output("\nGetting login page (user simulation).. with " + LoginUrl); - var loginPage = await webclient.GetString(myRequestLogin); - - // Build WebRequest for submitting authentification - var request = new WebRequest() - { - PostData = pairs, - Referer = LoginUrl, - Type = RequestType.POST, - Url = LoginCheckUrl, - Headers = emulatedBrowserHeaders, - Cookies = indexPage.Cookies, - - }; - - // Perform loggin - latencyNow(); - output("\nPerform loggin.. with " + LoginCheckUrl); - var response = await webclient.GetString(request); - - // Test if we are logged in - await ConfigureIfOK(response.Cookies, !string.IsNullOrEmpty(response.Cookies) && !response.IsRedirect, () => - { - // Default error message - string message = "Error during attempt !"; - - // Parse redirect header - string redirectTo = response.RedirectingTo; - - // Analyzer error code - if(redirectTo.Contains("login.php?error=4")) - { - // Set message - message = "Wrong username or password !"; - } - - // Oops, unable to login - output("-> Login failed: " + message, "error"); - throw new ExceptionWithConfigData("Login failed: " + message, configData); - }); - - output("\nCookies saved for future uses..."); - ConfigData.CookieHeader.Value = indexPage.Cookies + " " + response.Cookies + " ts_username=" + ConfigData.Username.Value; - - output("\n-> Login Success\n"); - - return IndexerConfigurationStatus.RequiresTesting; - } - - /// <summary> - /// Execute our search query - /// </summary> - /// <param name="query">Query</param> - /// <returns>Releases</returns> - public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query) - { - var releases = new List<ReleaseInfo>(); - var torrentRowList = new List<CQ>(); - var searchTerm = query.GetQueryString(); - var searchUrl = SearchUrl; - int nbResults = 0; - int pageLinkCount = 0; - - // Check cache first so we don't query the server (if search term used or not in dev mode) - if (!DevMode && !string.IsNullOrEmpty(searchTerm)) - { - lock (cache) - { - // Remove old cache items - CleanCache(); - - // Search in cache - var cachedResult = cache.Where(i => i.Query == searchTerm).FirstOrDefault(); - if (cachedResult != null) - return cachedResult.Results.Select(s => (ReleaseInfo)s.Clone()).ToArray(); - } - } - - // Build our query - var request = buildQuery(searchTerm, query, searchUrl); - - // Getting results & Store content - WebClientStringResult results = await queryExec(request); - fDom = results.Content; - - try - { - // Find torrent rows - var firstPageRows = findTorrentRows(); - - // Add them to torrents list - torrentRowList.AddRange(firstPageRows.Select(fRow => fRow.Cq())); - - // Check if there are pagination links at bottom - Boolean pagination = (fDom["#quicknavpage_menu"].Length != 0); - - // If pagination available - if (pagination) - { - // Retrieve available pages (3 pages shown max) - pageLinkCount = fDom["#navcontainer_f:first > ul"].Find("a").Not(".smalltext").Not("#quicknavpage").Length; - - // Last button ? (So more than 3 page are available) - Boolean more = (fDom["#navcontainer_f:first > ul"].Find("a.smalltext").Length > 1); ; - - // More page than 3 pages ? - if (more) - { - // Get total page count from last link - pageLinkCount = ParseUtil.CoerceInt(Regex.Match(fDom["#navcontainer_f:first > ul"].Find("a:eq(4)").Attr("href").ToString(), @"\d+").Value); - } - - // Calculate average number of results (based on torrents rows lenght on first page) - nbResults = firstPageRows.Count() * pageLinkCount; - } - else { - nbResults = 1; - pageLinkCount = 1; - - // Check if we have a minimum of one result - if (firstPageRows.Length > 1) - { - // Retrieve total count on our alone page - nbResults = firstPageRows.Count(); - } - else - { - // Check if no result - if(torrentRowList.First().Find("td").Length == 1) - { - // No results found - output("\nNo result found for your query, please try another search term ...\n", "info"); - - // No result found for this query - return releases; - } - } - } - output("\nFound " + nbResults + " result(s) (+/- " + firstPageRows.Length + ") in " + pageLinkCount + " page(s) for this query !"); - output("\nThere are " + firstPageRows.Length + " results on the first page !"); - - // If we have a term used for search and pagination result superior to one - if (!string.IsNullOrWhiteSpace(query.GetQueryString()) && pageLinkCount > 1) - { - // Starting with page #2 - for (int i = 2; i <= Math.Min(Int32.Parse(ConfigData.Pages.Value), pageLinkCount); i++) - { - output("\nProcessing page #" + i); - - // Request our page - latencyNow(); - - // Build our query -- Minus 1 to page due to strange pagination number on tracker side, starting with page 0... - var pageRequest = buildQuery(searchTerm, query, searchUrl, i); - - // Getting results & Store content - WebClientStringResult pageResults = await queryExec(pageRequest); - - // Assign response - fDom = pageResults.Content; - - // Process page results - var additionalPageRows = findTorrentRows(); - - // Add them to torrents list - torrentRowList.AddRange(additionalPageRows.Select(fRow => fRow.Cq())); - } - } - - // Loop on results - foreach (CQ tRow in torrentRowList) - { - output("\n=>> Torrent #" + (releases.Count + 1)); - - // ID - int id = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(1) > div:first > a").Attr("name").ToString(), @"\d+").Value); - output("ID: " + id); - - // Check if torrent is not nuked by tracker or rulez, can't download it - if (tRow.Find("td:eq(2) > a").Length == 0) - { - // Next item - output("Torrent is nuked, we can't download it, going to next torrent..."); - continue; - } - - // Release Name - string name = tRow.Find("td:eq(2) > a").Attr("title").ToString().Substring(24).Trim(); - output("Release: " + name); - - // Category - int categoryID = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(0) > a").Attr("href").ToString(), @"\d+").Value); - string categoryName = tRow.Find("td:eq(0) > a > img").Attr("title").Split(new[] { ':' }, 2)[1].Trim().ToString(); - output("Category: " + MapTrackerCatToNewznab(categoryID.ToString()) + " (" + categoryID + " - " + categoryName + ")"); - - // Seeders - int seeders = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(5) > div > font").Select(s => Regex.Replace(s.ToString(), "<.*?>", String.Empty)).ToString(), @"\d+").Value); - output("Seeders: " + seeders); - - // Leechers - int leechers = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(6) > div > font").Text().ToString(), @"\d+").Value); - output("Leechers: " + leechers); - - // Completed - int completed = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(4)").Text().ToString(), @"\d+").Value); - output("Completed: " + completed); - - // Files - int files = 1; - if (tRow.Find("td:eq(3) > a").Length == 1) - { - files = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(3) > a").Text().ToString(), @"\d+").Value); - } - output("Files: " + files); - - // Health - int percent = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(7) > img").Attr("src").ToString(), @"\d+").Value) * 10; - output("Health: " + percent + "%"); - - // Size - string humanSize = tRow.Find("td:eq(8)").Text().ToString().ToLowerInvariant(); - long size = ReleaseInfo.GetBytes(humanSize); - output("Size: " + humanSize + " (" + size + " bytes)"); - - // Date & IMDB & Genre - string infosData = tRow.Find("td:eq(1) > div:last").Text().ToString(); - IList<string> infosList = Regex.Split(infosData, "\\|").ToList(); - IList<string> infosTorrent = infosList.Select(s => s.Split(new[] { ':' }, 2)[1].Trim()).ToList(); - - // --> Date - DateTime date = formatDate(infosTorrent.First()); - output("Released on: " + date.ToLocalTime().ToString()); - - // --> Genre - string genre = infosTorrent.Last(); - output("Genre: " + genre); - - // Torrent Details URL - Uri detailsLink = new Uri(TorrentDescriptionUrl.Replace("{id}", id.ToString())); - output("Details: " + detailsLink.AbsoluteUri); - - // Torrent Comments URL - Uri commentsLink = new Uri(TorrentCommentUrl.Replace("{id}", id.ToString())); - output("Comments Link: " + commentsLink.AbsoluteUri); - - // Torrent Download URL - Uri downloadLink = new Uri(TorrentDownloadUrl.Replace("{id}", id.ToString())); - output("Download Link: " + downloadLink.AbsoluteUri); - - // Building release infos - var release = new ReleaseInfo(); - release.Category = MapTrackerCatToNewznab(categoryID.ToString()); - release.Title = name; - release.Seeders = seeders; - release.Peers = seeders + leechers; - release.MinimumRatio = 1; - release.MinimumSeedTime = 172800; - release.PublishDate = date; - release.Size = size; - release.Guid = detailsLink; - release.Comments = commentsLink; - release.Link = downloadLink; - releases.Add(release); - } - - } - catch (Exception ex) - { - OnParseError("Error, unable to parse result \n" + ex.StackTrace, ex); - } - - // Return found releases - return releases; - } - - /// <summary> - /// Build query to process - /// </summary> - /// <param name="term">Term to search</param> - /// <param name="query">Torznab Query for categories mapping</param> - /// <param name="url">Search url for provider</param> - /// <param name="page">Page number to request</param> - /// <returns>URL to query for parsing and processing results</returns> - private string buildQuery(string term, TorznabQuery query, string url, int page = 0) - { - var parameters = new NameValueCollection(); - List<string> categoriesList = MapTorznabCapsToTrackers(query); - - // Building our tracker query - parameters.Add("do", "search"); - - // If search term provided - if (!string.IsNullOrWhiteSpace(term)) - { - // Add search term ~~ Strange search engine, need to replace space with dot for results ! - parameters.Add("keywords", term.Replace(' ', '.')); - } - else - { - // Showing all torrents (just for output function) - parameters.Add("keywords", ""); - term = "all"; - } - - // Adding requested categories - if(categoriesList.Count > 0) - { - // Add categories - parameters.Add("category", String.Join(",", categoriesList)); - } - else - { - // Add empty category parameter - parameters.Add("category", ""); - } - - // Building our tracker query - parameters.Add("search_type", "t_name"); - - // Check if we are processing a new page - if (page > 1) - { - // Adding page number to query - parameters.Add("page", page.ToString()); - } - - // Building our query - url += "?" + parameters.GetQueryString(); - - output("\nBuilded query for \"" + term + "\"... " + url); - - // Return our search url - return url; - } - - /// <summary> - /// Switch Method for Querying - /// </summary> - /// <param name="request">URL created by Query Builder</param> - /// <returns>Results from query</returns> - private async Task<WebClientStringResult> queryExec(string request) - { - WebClientStringResult results = null; - - // Switch in we are in DEV mode with Hard Drive Cache or not - if (DevMode && CacheMode) - { - // Check Cache before querying and load previous results if available - results = await queryCache(request); - } - else - { - // Querying tracker directly - results = await queryTracker(request); - } - return results; - } - - /// <summary> - /// Get Torrents Page from Cache by Query Provided - /// </summary> - /// <param name="request">URL created by Query Builder</param> - /// <returns>Results from query</returns> - private async Task<WebClientStringResult> queryCache(string request) - { - WebClientStringResult results = null; - - // Create Directory if not exist - System.IO.Directory.CreateDirectory(directory); - - // Clean Storage Provider Directory from outdated cached queries - cleanCacheStorage(); - - // Create fingerprint for request - string file = directory + request.GetHashCode() + ".json"; - - // Checking modes states - if (System.IO.File.Exists(file)) - { - // File exist... loading it right now ! - output("Loading results from hard drive cache ..." + request.GetHashCode() + ".json"); - results = JsonConvert.DeserializeObject<WebClientStringResult>(System.IO.File.ReadAllText(file)); - } - else - { - // No cached file found, querying tracker directly - results = await queryTracker(request); - - // Cached file didn't exist for our query, writing it right now ! - output("Writing results to hard drive cache ..." + request.GetHashCode() + ".json"); - System.IO.File.WriteAllText(file, JsonConvert.SerializeObject(results)); - } - return results; - } - - /// <summary> - /// Get Torrents Page from Tracker by Query Provided - /// </summary> - /// <param name="request">URL created by Query Builder</param> - /// <returns>Results from query</returns> - private async Task<WebClientStringResult> queryTracker(string request) - { - WebClientStringResult results = null; - - // Cache mode not enabled or cached file didn't exist for our query - output("\nQuerying tracker for results...."); - - // Request our first page - latencyNow(); - results = await RequestStringWithCookiesAndRetry(request, ConfigData.CookieHeader.Value, SearchUrl, emulatedBrowserHeaders); - - // Return results from tracker - return results; - } - - /// <summary> - /// Clean Hard Drive Cache Storage - /// </summary> - /// <param name="force">Force Provider Folder deletion</param> - private void cleanCacheStorage(Boolean force = false) - { - // Check cleaning method - if (force) - { - // Deleting Provider Storage folder and all files recursively - output("\nDeleting Provider Storage folder and all files recursively ..."); - - // Check if directory exist - if (System.IO.Directory.Exists(directory)) - { - // Delete storage directory of provider - System.IO.Directory.Delete(directory, true); - output("-> Storage folder deleted successfully."); - } - else - { - // No directory, so nothing to do - output("-> No Storage folder found for this provider !"); - } - } - else - { - int i = 0; - // Check if there is file older than ... and delete them - output("\nCleaning Provider Storage folder... in progress."); - System.IO.Directory.GetFiles(directory) - .Select(f => new System.IO.FileInfo(f)) - .Where(f => f.LastAccessTime < DateTime.Now.AddMilliseconds(-Convert.ToInt32(ConfigData.HardDriveCacheKeepTime.Value))) - .ToList() - .ForEach(f => { - output("Deleting cached file << " + f.Name + " >> ... done."); - f.Delete(); - i++; - }); - - // Inform on what was cleaned during process - if (i > 0) - { - output("-> Deleted " + i + " cached files during cleaning."); - } - else { - output("-> Nothing deleted during cleaning."); - } - } - } - - /// <summary> - /// Generate a random fake latency to avoid detection on tracker side - /// </summary> - private void latencyNow() - { - // Need latency ? - if (Latency) - { - // Generate a random value in our range - var random = new Random(DateTime.Now.Millisecond); - int waiting = random.Next(Convert.ToInt32(ConfigData.LatencyStart.Value), Convert.ToInt32(ConfigData.LatencyEnd.Value)); - output("\nLatency Faker => Sleeping for " + waiting + " ms..."); - - // Sleep now... - System.Threading.Thread.Sleep(waiting); - } - } - - /// <summary> - /// Find torrent rows in search pages - /// </summary> - /// <returns>JQuery Object</returns> - private CQ findTorrentRows() - { - // Return all occurencis of torrents found - return fDom["#showcontents > table > tbody > tr:not(:first)"]; - } - - /// <summary> - /// Format Date to DateTime - /// </summary> - /// <param name="clock"></param> - /// <returns>A DateTime</returns> - private DateTime formatDate(string clock) - { - DateTime date; - - // Switch from date format - if(clock.Contains("Aujourd'hui") || clock.Contains("Hier")) - { - // Get hours & minutes - IList<int> infosClock = clock.Split(':').Select(s => ParseUtil.CoerceInt(Regex.Match(s, @"\d+").Value)).ToList(); - - // Ago date with today - date = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, Convert.ToInt32(infosClock[0]), Convert.ToInt32(infosClock[1]), DateTime.Now.Second); - - // Set yesterday if necessary - if (clock.Contains("Hier")) - { - // Remove one day from date - date.AddDays(-1); - } - } - else - { - // Parse Date if full - date = DateTime.ParseExact(clock, "MM-dd-yyyy HH:mm", CultureInfo.GetCultureInfo("fr-FR"), DateTimeStyles.AssumeLocal); - } - - return date.ToUniversalTime(); - } - - /// <summary> - /// Download torrent file from tracker - /// </summary> - /// <param name="link">URL string</param> - /// <returns></returns> - public async override Task<byte[]> Download(Uri link) - { - var dl = link.AbsoluteUri; - // This tracker need to thanks Uploader before getting torrent file... - output("\nThis tracker needs you to thank uploader before downloading torrent!"); - - // Retrieving ID from link provided - int id = ParseUtil.CoerceInt(Regex.Match(link.AbsoluteUri, @"\d+").Value); - output("Torrent Requested ID: " + id); - - // Building login form data - var pairs = new Dictionary<string, string> { - { "torrentid", id.ToString() }, - { "_", string.Empty } // ~~ Strange, blank param... - }; - - // Add emulated XHR request - emulatedBrowserHeaders.Add("X-Prototype-Version", "1.6.0.3"); - emulatedBrowserHeaders.Add("X-Requested-With", "XMLHttpRequest"); - - // Build WebRequest for thanks - var myRequestThanks = new WebRequest() - { - Type = RequestType.POST, - PostData = pairs, - Url = TorrentThanksUrl, - Headers = emulatedBrowserHeaders, - Cookies = ConfigData.CookieHeader.Value, - Referer = TorrentDescriptionUrl.Replace("{id}", id.ToString()) - }; - - // Get thanks page -- (not used, just for doing a request) - latencyNow(); - output("Thanks user, to get download link for our torrent.. with " + TorrentThanksUrl); - var thanksPage = await webclient.GetString(myRequestThanks); - - // Get torrent file now - output("Getting torrent file now...."); - var response = await base.Download(link); - - // Remove our XHR request header - emulatedBrowserHeaders.Remove("X-Prototype-Version"); - emulatedBrowserHeaders.Remove("X-Requested-With"); - - // Return content - return response; - } - - /// <summary> - /// Output message for logging or developpment (console) - /// </summary> - /// <param name="message">Message to output</param> - /// <param name="level">Level for Logger</param> - private void output(string message, string level = "debug") - { - // Check if we are in dev mode - if (DevMode) - { - // Output message to console - Console.WriteLine(message); - } - else - { - // Send message to logger with level - switch (level) - { - default: - goto case "debug"; - case "debug": - // Only if Debug Level Enabled on Jackett - if (Engine.Logger.IsDebugEnabled) - { - logger.Debug(message); - } - break; - case "info": - logger.Info(message); - break; - case "error": - logger.Error(message); - break; - } - } - } - - /// <summary> - /// Validate Config entered by user on Jackett - /// </summary> - private void validateConfig() - { - output("\nValidating Settings ... \n"); - - // Check Username Setting - if (string.IsNullOrEmpty(ConfigData.Username.Value)) - { - throw new ExceptionWithConfigData("You must provide a username for this tracker to login !", ConfigData); - } - else - { - output("Validated Setting -- Username (auth) => " + ConfigData.Username.Value.ToString()); - } - - // Check Password Setting - if (string.IsNullOrEmpty(ConfigData.Password.Value)) - { - throw new ExceptionWithConfigData("You must provide a password with your username for this tracker to login !", ConfigData); - } - else - { - output("Validated Setting -- Password (auth) => " + ConfigData.Password.Value.ToString()); - } - - // Check Max Page Setting - if (!string.IsNullOrEmpty(ConfigData.Pages.Value)) - { - try - { - output("Validated Setting -- Max Pages => " + Convert.ToInt32(ConfigData.Pages.Value)); - } - catch (Exception) - { - throw new ExceptionWithConfigData("Please enter a numeric maximum number of pages to crawl !", ConfigData); - } - } - else - { - throw new ExceptionWithConfigData("Please enter a maximum number of pages to crawl !", ConfigData); - } - - // Check Latency Setting - if (ConfigData.Latency.Value) - { - output("\nValidated Setting -- Latency Simulation enabled"); - - // Check Latency Start Setting - if (!string.IsNullOrEmpty(ConfigData.LatencyStart.Value)) - { - try - { - output("Validated Setting -- Latency Start => " + Convert.ToInt32(ConfigData.LatencyStart.Value)); - } - catch (Exception) - { - throw new ExceptionWithConfigData("Please enter a numeric latency start in ms !", ConfigData); - } - } - else - { - throw new ExceptionWithConfigData("Latency Simulation enabled, Please enter a start latency !", ConfigData); - } - - // Check Latency End Setting - if (!string.IsNullOrEmpty(ConfigData.LatencyEnd.Value)) - { - try - { - output("Validated Setting -- Latency End => " + Convert.ToInt32(ConfigData.LatencyEnd.Value)); - } - catch (Exception) - { - throw new ExceptionWithConfigData("Please enter a numeric latency end in ms !", ConfigData); - } - } - else - { - throw new ExceptionWithConfigData("Latency Simulation enabled, Please enter a end latency !", ConfigData); - } - } - - // Check Browser Setting - if (ConfigData.Browser.Value == true) - { - output("\nValidated Setting -- Browser Simulation enabled"); - - // Check ACCEPT header Setting - if (string.IsNullOrEmpty(ConfigData.HeaderAccept.Value)) - { - throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an ACCEPT header !", ConfigData); - } - else - { - output("Validated Setting -- ACCEPT (header) => " + ConfigData.HeaderAccept.Value.ToString()); - } - - // Check ACCEPT-LANG header Setting - if (string.IsNullOrEmpty(ConfigData.HeaderAcceptLang.Value)) - { - throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an ACCEPT-LANG header !", ConfigData); - } - else - { - output("Validated Setting -- ACCEPT-LANG (header) => " + ConfigData.HeaderAcceptLang.Value.ToString()); - } - - // Check USER-AGENT header Setting - if (string.IsNullOrEmpty(ConfigData.HeaderUserAgent.Value)) - { - throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an USER-AGENT header !", ConfigData); - } - else - { - output("Validated Setting -- USER-AGENT (header) => " + ConfigData.HeaderUserAgent.Value.ToString()); - } - } - else - { - // Browser simulation must be enabled (otherwhise, this provider will not work due to tracker's security) - throw new ExceptionWithConfigData("Browser Simulation must be enabled for this provider to work, please enable it !", ConfigData); - } - - // Check Dev Cache Settings - if (ConfigData.HardDriveCache.Value == true) - { - output("\nValidated Setting -- DEV Hard Drive Cache enabled"); - - // Check if Dev Mode enabled ! - if (!ConfigData.DevMode.Value) - { - throw new ExceptionWithConfigData("Hard Drive is enabled but not in DEV MODE, Please enable DEV MODE !", ConfigData); - } - - // Check Cache Keep Time Setting - if (!string.IsNullOrEmpty(ConfigData.HardDriveCacheKeepTime.Value)) - { - try - { - output("Validated Setting -- Cache Keep Time (ms) => " + Convert.ToInt32(ConfigData.HardDriveCacheKeepTime.Value)); - } - catch (Exception) - { - throw new ExceptionWithConfigData("Please enter a numeric hard drive keep time in ms !", ConfigData); - } - } - else - { - throw new ExceptionWithConfigData("Hard Drive Cache enabled, Please enter a maximum keep time for cache !", ConfigData); - } - } - else - { - // Delete cache if previously existed - cleanCacheStorage(true); - } - } - } +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using CsQuery; +using Jackett.Models; +using Jackett.Models.IndexerConfig.Bespoke; +using Jackett.Services; +using Jackett.Utils; +using Jackett.Utils.Clients; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NLog; + +namespace Jackett.Indexers +{ + /// <summary> + /// Provider for French-ADN Private Tracker + /// </summary> + public class FrenchAdn : BaseIndexer, IIndexer + { + private string LoginUrl => SiteLink + "login.php?"; + private string LoginCheckUrl => SiteLink + "takelogin.php"; + private string SearchUrl => SiteLink + "browse.php"; + private string TorrentCommentUrl => SiteLink + "details.php?id={id}#comments"; + private string TorrentDescriptionUrl => SiteLink + "details.php?id={id}"; + private string TorrentDownloadUrl => SiteLink + "download.php?id={id}"; + private string TorrentThanksUrl => SiteLink + "takethanks.php"; + private bool Latency => ConfigData.Latency.Value; + private bool DevMode => ConfigData.DevMode.Value; + private bool CacheMode => ConfigData.HardDriveCache.Value; + private static string Directory => System.IO.Path.GetTempPath() + "Jackett\\" + MethodBase.GetCurrentMethod().DeclaringType?.Name + "\\"; + + private readonly Dictionary<string, string> _emulatedBrowserHeaders = new Dictionary<string, string>(); + private CQ _fDom; + private ConfigurationDataFrenchAdn ConfigData => (ConfigurationDataFrenchAdn)configData; + + public FrenchAdn(IIndexerManagerService i, IWebClient w, Logger l, IProtectionService ps) + : base( + name: "French-ADN", + description: "Your French Family Provider", + link: "https://french-adn.com/", + caps: new TorznabCapabilities(), + manager: i, + client: w, + logger: l, + p: ps, + downloadBase: "https://french-adn.com/download.php?id=", + configData: new ConfigurationDataFrenchAdn()) + { + // Clean capabilities + TorznabCaps.Categories.Clear(); + + // Movies + AddCategoryMapping("15", TorznabCatType.Movies); // ALL + AddCategoryMapping("108", TorznabCatType.MoviesSD); // TS CAM + AddCategoryMapping("25", TorznabCatType.MoviesSD); // BDRIP + AddCategoryMapping("56", TorznabCatType.MoviesSD); // BRRIP + AddCategoryMapping("16", TorznabCatType.MoviesSD); // DVDRIP + AddCategoryMapping("49", TorznabCatType.MoviesDVD); // TVRIP + AddCategoryMapping("102", TorznabCatType.MoviesWEBDL); // WEBRIP + AddCategoryMapping("105", TorznabCatType.MoviesHD); // 1080P + AddCategoryMapping("104", TorznabCatType.MoviesHD); // 720P + AddCategoryMapping("17", TorznabCatType.MoviesDVD); // DVD R + AddCategoryMapping("21", TorznabCatType.MoviesDVD); // DVD R5 + AddCategoryMapping("112", TorznabCatType.MoviesDVD); // DVD REMUX + AddCategoryMapping("107", TorznabCatType.Movies3D); // 3D + AddCategoryMapping("113", TorznabCatType.MoviesBluRay); // BLURAY + AddCategoryMapping("118", TorznabCatType.MoviesHD); // MHD + + // Series + AddCategoryMapping("41", TorznabCatType.TV); // ALL + AddCategoryMapping("43", TorznabCatType.TV); // VF + AddCategoryMapping("44", TorznabCatType.TV); // VOSTFR + AddCategoryMapping("42", TorznabCatType.TV); // PACK + + // TV + AddCategoryMapping("110", TorznabCatType.TV); // SHOWS + + // Anime + AddCategoryMapping("109", TorznabCatType.TVAnime); // ANIME + + // Manga + AddCategoryMapping("119", TorznabCatType.TVAnime); // MANGA + + // Documentaries + AddCategoryMapping("114", TorznabCatType.TVDocumentary); // DOCUMENTARY + + // Music + AddCategoryMapping("22", TorznabCatType.Audio); // ALL + AddCategoryMapping("24", TorznabCatType.AudioLossless); // FLAC + AddCategoryMapping("23", TorznabCatType.AudioMP3); // MP3 + + // Games + AddCategoryMapping("33", TorznabCatType.PCGames); // ALL + AddCategoryMapping("45", TorznabCatType.PCGames); // PC GAMES + AddCategoryMapping("93", TorznabCatType.Console3DS); // 3DS + AddCategoryMapping("94", TorznabCatType.Console); // PS2 + AddCategoryMapping("93", TorznabCatType.ConsolePS3); // PS3 + AddCategoryMapping("95", TorznabCatType.ConsolePSP); // PSP + AddCategoryMapping("35", TorznabCatType.ConsolePS3); // WII + + // Applications + AddCategoryMapping("11", TorznabCatType.PC); // ALL + AddCategoryMapping("12", TorznabCatType.PC); // APPS WINDOWS + AddCategoryMapping("97", TorznabCatType.PCMac); // APPS MAC + AddCategoryMapping("98", TorznabCatType.PC); // APPS LINUX + + // Books + AddCategoryMapping("115", TorznabCatType.BooksEbook); // EBOOK + AddCategoryMapping("114", TorznabCatType.BooksComics); // COMICS + + // Other + AddCategoryMapping("103", TorznabCatType.Other); // STAFF + } + + /// <summary> + /// Configure our FADN Provider + /// </summary> + /// <param name="configJson">Our params in Json</param> + /// <returns>Configuration state</returns> + public async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson) + { + // Retrieve config values set by Jackett's user + ConfigData.LoadValuesFromJson(configJson); + + // Check & Validate Config + ValidateConfig(); + + // Setting our data for a better emulated browser (maximum security) + // TODO: Encoded Content not supported by Jackett at this time + // emulatedBrowserHeaders.Add("Accept-Encoding", "gzip, deflate"); + + // If we want to simulate a browser + if (ConfigData.Browser.Value) + { + + // Clean headers + _emulatedBrowserHeaders.Clear(); + + // Inject headers + _emulatedBrowserHeaders.Add("Accept", ConfigData.HeaderAccept.Value); + _emulatedBrowserHeaders.Add("Accept-Language", ConfigData.HeaderAcceptLang.Value); + _emulatedBrowserHeaders.Add("DNT", Convert.ToInt32(ConfigData.HeaderDnt.Value).ToString()); + _emulatedBrowserHeaders.Add("Upgrade-Insecure-Requests", Convert.ToInt32(ConfigData.HeaderUpgradeInsecure.Value).ToString()); + _emulatedBrowserHeaders.Add("User-Agent", ConfigData.HeaderUserAgent.Value); + } + + await DoLogin(); + + return IndexerConfigurationStatus.RequiresTesting; + } + + /// <summary> + /// Perform login to racker + /// </summary> + /// <returns></returns> + private async Task DoLogin() + { + // Build WebRequest for index + var myIndexRequest = new WebRequest() + { + Type = RequestType.GET, + Url = SiteLink, + Headers = _emulatedBrowserHeaders + }; + + // Get index page for cookies + Output("\nGetting index page (for cookies).. with " + SiteLink); + var indexPage = await webclient.GetString(myIndexRequest); + + // Building login form data + var pairs = new Dictionary<string, string> { + { "username", ConfigData.Username.Value }, + { "password", ConfigData.Password.Value } + }; + + // Build WebRequest for login + var myRequestLogin = new WebRequest() + { + Type = RequestType.GET, + Url = LoginUrl, + Headers = _emulatedBrowserHeaders, + Cookies = indexPage.Cookies, + Referer = SiteLink + }; + + // Get login page -- (not used, but simulation needed by tracker security's checks) + LatencyNow(); + Output("\nGetting login page (user simulation).. with " + LoginUrl); + await webclient.GetString(myRequestLogin); + + // Build WebRequest for submitting authentification + var request = new WebRequest() + { + PostData = pairs, + Referer = LoginUrl, + Type = RequestType.POST, + Url = LoginCheckUrl, + Headers = _emulatedBrowserHeaders, + Cookies = indexPage.Cookies, + + }; + + // Perform loggin + LatencyNow(); + Output("\nPerform loggin.. with " + LoginCheckUrl); + var response = await webclient.GetString(request); + + // Test if we are logged in + await ConfigureIfOK(response.Cookies, !string.IsNullOrEmpty(response.Cookies) && !response.IsRedirect, () => + { + // Default error message + var message = "Error during attempt !"; + + // Parse redirect header + var redirectTo = response.RedirectingTo; + + // Analyzer error code + if (redirectTo.Contains("login.php?error=4")) + { + // Set message + message = "Wrong username or password !"; + } + + // Oops, unable to login + Output("-> Login failed: " + message, "error"); + throw new ExceptionWithConfigData("Login failed: " + message, configData); + }); + + Output("\nCookies saved for future uses..."); + ConfigData.CookieHeader.Value = indexPage.Cookies + " " + response.Cookies + " ts_username=" + ConfigData.Username.Value; + + Output("\n-> Login Success\n"); + } + + /// <summary> + /// Check logged-in state for provider + /// </summary> + /// <returns></returns> + private async Task CheckLogin() + { + // Checking ... + Output("\n-> Checking logged-in state...."); + var loggedInCheck = await RequestStringWithCookies(SearchUrl); + if (!loggedInCheck.Content.Contains("/logout.php")) + { + // Cookie expired, renew session on provider + Output("-> Not logged, login now...\n"); + await DoLogin(); + } + else + { + // Already logged, session active + Output("-> Already logged, continue...\n"); + } + } + + /// <summary> + /// Execute our search query + /// </summary> + /// <param name="query">Query</param> + /// <returns>Releases</returns> + public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query) + { + var releases = new List<ReleaseInfo>(); + var torrentRowList = new List<CQ>(); + var searchTerm = query.GetQueryString(); + var searchUrl = SearchUrl; + + // Check login before performing a query + await CheckLogin(); + + // Check cache first so we don't query the server (if search term used or not in dev mode) + if (!DevMode && !string.IsNullOrEmpty(searchTerm)) + { + lock (cache) + { + // Remove old cache items + CleanCache(); + + // Search in cache + var cachedResult = cache.FirstOrDefault(i => i.Query == searchTerm); + if (cachedResult != null) + return cachedResult.Results.Select(s => (ReleaseInfo)s.Clone()).ToArray(); + } + } + + // Build our query + var request = BuildQuery(searchTerm, query, searchUrl); + + // Getting results & Store content + var results = await QueryExec(request); + _fDom = results.Content; + + try + { + // Find torrent rows + var firstPageRows = FindTorrentRows(); + + // Add them to torrents list + torrentRowList.AddRange(firstPageRows.Select(fRow => fRow.Cq())); + + // Check if there are pagination links at bottom + var pagination = (_fDom["#quicknavpage_menu"].Length != 0); + + // If pagination available + int nbResults; + int pageLinkCount; + if (pagination) + { + // Retrieve available pages (3 pages shown max) + pageLinkCount = _fDom["#navcontainer_f:first > ul"].Find("a").Not(".smalltext").Not("#quicknavpage").Length; + + // Last button ? (So more than 3 page are available) + var more = _fDom["#navcontainer_f:first > ul"].Find("a.smalltext").Length > 1; + + // More page than 3 pages ? + if (more) + { + // Get total page count from last link + pageLinkCount = ParseUtil.CoerceInt(Regex.Match(_fDom["#navcontainer_f:first > ul"].Find("a:eq(4)").Attr("href"), @"\d+").Value); + } + + // Calculate average number of results (based on torrents rows lenght on first page) + nbResults = firstPageRows.Count() * pageLinkCount; + } + else { + nbResults = 1; + pageLinkCount = 1; + + // Check if we have a minimum of one result + if (firstPageRows.Length > 1) + { + // Retrieve total count on our alone page + nbResults = firstPageRows.Count(); + } + else + { + // Check if no result + if(torrentRowList.First().Find("td").Length == 1) + { + // No results found + Output("\nNo result found for your query, please try another search term ...\n", "info"); + + // No result found for this query + return releases; + } + } + } + Output("\nFound " + nbResults + " result(s) (+/- " + firstPageRows.Length + ") in " + pageLinkCount + " page(s) for this query !"); + Output("\nThere are " + firstPageRows.Length + " results on the first page !"); + + // If we have a term used for search and pagination result superior to one + if (!string.IsNullOrWhiteSpace(query.GetQueryString()) && pageLinkCount > 1) + { + // Starting with page #2 + for (var i = 2; i <= Math.Min(int.Parse(ConfigData.Pages.Value), pageLinkCount); i++) + { + Output("\nProcessing page #" + i); + + // Request our page + LatencyNow(); + + // Build our query -- Minus 1 to page due to strange pagination number on tracker side, starting with page 0... + var pageRequest = BuildQuery(searchTerm, query, searchUrl, i); + + // Getting results & Store content + WebClientStringResult pageResults = await QueryExec(pageRequest); + + // Assign response + _fDom = pageResults.Content; + + // Process page results + var additionalPageRows = FindTorrentRows(); + + // Add them to torrents list + torrentRowList.AddRange(additionalPageRows.Select(fRow => fRow.Cq())); + } + } + + // Loop on results + foreach (var tRow in torrentRowList) + { + Output("\n=>> Torrent #" + (releases.Count + 1)); + + // ID + var id = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(1) > div:first > a").Attr("name"), @"\d+").Value); + Output("ID: " + id); + + // Check if torrent is not nuked by tracker or rulez, can't download it + if (tRow.Find("td:eq(2) > a").Length == 0) + { + // Next item + Output("Torrent is nuked, we can't download it, going to next torrent..."); + continue; + } + + // Release Name + var name = tRow.Find("td:eq(2) > a").Attr("title").Substring(24).Trim(); + Output("Release: " + name); + + // Category + var categoryId = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(0) > a").Attr("href"), @"\d+").Value); + var categoryName = tRow.Find("td:eq(0) > a > img").Attr("title").Split(new[] { ':' }, 2)[1].Trim(); + Output("Category: " + MapTrackerCatToNewznab(categoryId.ToString()) + " (" + categoryId + " - " + categoryName + ")"); + + // Seeders + var seeders = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(5) > div > font").Select(s => Regex.Replace(s.ToString(), "<.*?>", string.Empty)).ToString(), @"\d+").Value); + Output("Seeders: " + seeders); + + // Leechers + var leechers = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(6) > div > font").Text(), @"\d+").Value); + Output("Leechers: " + leechers); + + // Completed + var completed = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(4)").Text(), @"\d+").Value); + Output("Completed: " + completed); + + // Files + var files = 1; + if (tRow.Find("td:eq(3) > a").Length == 1) + { + files = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(3) > a").Text(), @"\d+").Value); + } + Output("Files: " + files); + + // Health + var percent = ParseUtil.CoerceInt(Regex.Match(tRow.Find("td:eq(7) > img").Attr("src"), @"\d+").Value) * 10; + Output("Health: " + percent + "%"); + + // Size + var humanSize = tRow.Find("td:eq(8)").Text().ToLowerInvariant(); + var size = ReleaseInfo.GetBytes(humanSize); + Output("Size: " + humanSize + " (" + size + " bytes)"); + + // Date & IMDB & Genre + var infosData = tRow.Find("td:eq(1) > div:last").Text(); + var infosList = Regex.Split(infosData, "\\|").ToList(); + var infosTorrent = infosList.Select(s => s.Split(new[] { ':' }, 2)[1].Trim()).ToList(); + + // --> Date + var date = FormatDate(infosTorrent.First()); + Output("Released on: " + date.ToLocalTime()); + + // --> Genre + var genre = infosTorrent.Last(); + Output("Genre: " + genre); + + // Torrent Details URL + var detailsLink = new Uri(TorrentDescriptionUrl.Replace("{id}", id.ToString())); + Output("Details: " + detailsLink.AbsoluteUri); + + // Torrent Comments URL + var commentsLink = new Uri(TorrentCommentUrl.Replace("{id}", id.ToString())); + Output("Comments Link: " + commentsLink.AbsoluteUri); + + // Torrent Download URL + var downloadLink = new Uri(TorrentDownloadUrl.Replace("{id}", id.ToString())); + Output("Download Link: " + downloadLink.AbsoluteUri); + + // Building release infos + var release = new ReleaseInfo + { + Category = MapTrackerCatToNewznab(categoryId.ToString()), + Title = name, + Seeders = seeders, + Peers = seeders + leechers, + MinimumRatio = 1, + MinimumSeedTime = 172800, + PublishDate = date, + Size = size, + Guid = detailsLink, + Comments = commentsLink, + Link = downloadLink + }; + releases.Add(release); + } + + } + catch (Exception ex) + { + OnParseError("Error, unable to parse result \n" + ex.StackTrace, ex); + } + + // Return found releases + return releases; + } + + /// <summary> + /// Build query to process + /// </summary> + /// <param name="term">Term to search</param> + /// <param name="query">Torznab Query for categories mapping</param> + /// <param name="url">Search url for provider</param> + /// <param name="page">Page number to request</param> + /// <returns>URL to query for parsing and processing results</returns> + private string BuildQuery(string term, TorznabQuery query, string url, int page = 0) + { + var parameters = new NameValueCollection(); + var categoriesList = MapTorznabCapsToTrackers(query); + + // Building our tracker query + parameters.Add("do", "search"); + + // If search term provided + if (!string.IsNullOrWhiteSpace(term)) + { + // Add search term ~~ Strange search engine, need to replace space with dot for results ! + parameters.Add("keywords", term.Replace(' ', '.')); + } + else + { + // Showing all torrents (just for output function) + parameters.Add("keywords", ""); + term = "all"; + } + + // Adding requested categories + parameters.Add("category", categoriesList.Count > 0 ? string.Join(",", categoriesList) : ""); + + // Building our tracker query + parameters.Add("search_type", "t_name"); + + // Check if we are processing a new page + if (page > 1) + { + // Adding page number to query + parameters.Add("page", page.ToString()); + } + + // Building our query + url += "?" + parameters.GetQueryString(); + + Output("\nBuilded query for \"" + term + "\"... " + url); + + // Return our search url + return url; + } + + /// <summary> + /// Switch Method for Querying + /// </summary> + /// <param name="request">URL created by Query Builder</param> + /// <returns>Results from query</returns> + private async Task<WebClientStringResult> QueryExec(string request) + { + WebClientStringResult results; + + // Switch in we are in DEV mode with Hard Drive Cache or not + if (DevMode && CacheMode) + { + // Check Cache before querying and load previous results if available + results = await QueryCache(request); + } + else + { + // Querying tracker directly + results = await QueryTracker(request); + } + return results; + } + + /// <summary> + /// Get Torrents Page from Cache by Query Provided + /// </summary> + /// <param name="request">URL created by Query Builder</param> + /// <returns>Results from query</returns> + private async Task<WebClientStringResult> QueryCache(string request) + { + WebClientStringResult results; + + // Create Directory if not exist + System.IO.Directory.CreateDirectory(Directory); + + // Clean Storage Provider Directory from outdated cached queries + CleanCacheStorage(); + + // Create fingerprint for request + var file = Directory + request.GetHashCode() + ".json"; + + // Checking modes states + if (System.IO.File.Exists(file)) + { + // File exist... loading it right now ! + Output("Loading results from hard drive cache ..." + request.GetHashCode() + ".json"); + results = JsonConvert.DeserializeObject<WebClientStringResult>(System.IO.File.ReadAllText(file)); + } + else + { + // No cached file found, querying tracker directly + results = await QueryTracker(request); + + // Cached file didn't exist for our query, writing it right now ! + Output("Writing results to hard drive cache ..." + request.GetHashCode() + ".json"); + System.IO.File.WriteAllText(file, JsonConvert.SerializeObject(results)); + } + return results; + } + + /// <summary> + /// Get Torrents Page from Tracker by Query Provided + /// </summary> + /// <param name="request">URL created by Query Builder</param> + /// <returns>Results from query</returns> + private async Task<WebClientStringResult> QueryTracker(string request) + { + // Cache mode not enabled or cached file didn't exist for our query + Output("\nQuerying tracker for results...."); + + // Request our first page + LatencyNow(); + var results = await RequestStringWithCookiesAndRetry(request, ConfigData.CookieHeader.Value, SearchUrl, _emulatedBrowserHeaders); + + // Return results from tracker + return results; + } + + /// <summary> + /// Clean Hard Drive Cache Storage + /// </summary> + /// <param name="force">Force Provider Folder deletion</param> + private void CleanCacheStorage(bool force = false) + { + // Check cleaning method + if (force) + { + // Deleting Provider Storage folder and all files recursively + Output("\nDeleting Provider Storage folder and all files recursively ..."); + + // Check if directory exist + if (System.IO.Directory.Exists(Directory)) + { + // Delete storage directory of provider + System.IO.Directory.Delete(Directory, true); + Output("-> Storage folder deleted successfully."); + } + else + { + // No directory, so nothing to do + Output("-> No Storage folder found for this provider !"); + } + } + else + { + var i = 0; + // Check if there is file older than ... and delete them + Output("\nCleaning Provider Storage folder... in progress."); + System.IO.Directory.GetFiles(Directory) + .Select(f => new System.IO.FileInfo(f)) + .Where(f => f.LastAccessTime < DateTime.Now.AddMilliseconds(-Convert.ToInt32(ConfigData.HardDriveCacheKeepTime.Value))) + .ToList() + .ForEach(f => { + Output("Deleting cached file << " + f.Name + " >> ... done."); + f.Delete(); + i++; + }); + + // Inform on what was cleaned during process + if (i > 0) + { + Output("-> Deleted " + i + " cached files during cleaning."); + } + else { + Output("-> Nothing deleted during cleaning."); + } + } + } + + /// <summary> + /// Generate a random fake latency to avoid detection on tracker side + /// </summary> + private void LatencyNow() + { + // Need latency ? + if (Latency) + { + var random = new Random(DateTime.Now.Millisecond); + var waiting = random.Next(Convert.ToInt32(ConfigData.LatencyStart.Value), + Convert.ToInt32(ConfigData.LatencyEnd.Value)); + Output("\nLatency Faker => Sleeping for " + waiting + " ms..."); + + // Sleep now... + System.Threading.Thread.Sleep(waiting); + } + // Generate a random value in our range + } + + /// <summary> + /// Find torrent rows in search pages + /// </summary> + /// <returns>JQuery Object</returns> + private CQ FindTorrentRows() + { + // Return all occurencis of torrents found + return _fDom["#showcontents > table > tbody > tr:not(:first)"]; + } + + /// <summary> + /// Format Date to DateTime + /// </summary> + /// <param name="clock"></param> + /// <returns>A DateTime</returns> + private static DateTime FormatDate(string clock) + { + DateTime date; + + // Switch from date format + if(clock.Contains("Aujourd'hui") || clock.Contains("Hier")) + { + // Get hours & minutes + IList<int> infosClock = clock.Split(':').Select(s => ParseUtil.CoerceInt(Regex.Match(s, @"\d+").Value)).ToList(); + + // Ago date with today + date = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, Convert.ToInt32(infosClock[0]), Convert.ToInt32(infosClock[1]), DateTime.Now.Second); + + // Set yesterday if necessary + if (clock.Contains("Hier")) + { + // Remove one day from date + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + date.AddDays(-1); + } + } + else + { + // Parse Date if full + date = DateTime.ParseExact(clock, "MM-dd-yyyy HH:mm", CultureInfo.GetCultureInfo("fr-FR"), DateTimeStyles.AssumeLocal); + } + + return date.ToUniversalTime(); + } + + /// <summary> + /// Download torrent file from tracker + /// </summary> + /// <param name="link">URL string</param> + /// <returns></returns> + public override async Task<byte[]> Download(Uri link) + { + // This tracker need to thanks Uploader before getting torrent file... + Output("\nThis tracker needs you to thank uploader before downloading torrent!"); + + // Retrieving ID from link provided + var id = ParseUtil.CoerceInt(Regex.Match(link.AbsoluteUri, @"\d+").Value); + Output("Torrent Requested ID: " + id); + + // Building login form data + var pairs = new Dictionary<string, string> { + { "torrentid", id.ToString() }, + { "_", string.Empty } // ~~ Strange, blank param... + }; + + // Add emulated XHR request + _emulatedBrowserHeaders.Add("X-Prototype-Version", "1.6.0.3"); + _emulatedBrowserHeaders.Add("X-Requested-With", "XMLHttpRequest"); + + // Build WebRequest for thanks + var myRequestThanks = new WebRequest() + { + Type = RequestType.POST, + PostData = pairs, + Url = TorrentThanksUrl, + Headers = _emulatedBrowserHeaders, + Cookies = ConfigData.CookieHeader.Value, + Referer = TorrentDescriptionUrl.Replace("{id}", id.ToString()) + }; + + // Get thanks page -- (not used, just for doing a request) + LatencyNow(); + Output("Thanks user, to get download link for our torrent.. with " + TorrentThanksUrl); + await webclient.GetString(myRequestThanks); + + // Get torrent file now + Output("Getting torrent file now...."); + var response = await base.Download(link); + + // Remove our XHR request header + _emulatedBrowserHeaders.Remove("X-Prototype-Version"); + _emulatedBrowserHeaders.Remove("X-Requested-With"); + + // Return content + return response; + } + + /// <summary> + /// Output message for logging or developpment (console) + /// </summary> + /// <param name="message">Message to output</param> + /// <param name="level">Level for Logger</param> + private void Output(string message, string level = "debug") + { + // Check if we are in dev mode + if (DevMode) + { + // Output message to console + Console.WriteLine(message); + } + else + { + // Send message to logger with level + switch (level) + { + default: + goto case "debug"; + case "debug": + // Only if Debug Level Enabled on Jackett + if (Engine.Logger.IsDebugEnabled) + { + logger.Debug(message); + } + break; + case "info": + logger.Info(message); + break; + case "error": + logger.Error(message); + break; + } + } + } + + /// <summary> + /// Validate Config entered by user on Jackett + /// </summary> + private void ValidateConfig() + { + Output("\nValidating Settings ... \n"); + + // Check Username Setting + if (string.IsNullOrEmpty(ConfigData.Username.Value)) + { + throw new ExceptionWithConfigData("You must provide a username for this tracker to login !", ConfigData); + } + else + { + Output("Validated Setting -- Username (auth) => " + ConfigData.Username.Value); + } + + // Check Password Setting + if (string.IsNullOrEmpty(ConfigData.Password.Value)) + { + throw new ExceptionWithConfigData("You must provide a password with your username for this tracker to login !", ConfigData); + } + else + { + Output("Validated Setting -- Password (auth) => " + ConfigData.Password.Value); + } + + // Check Max Page Setting + if (!string.IsNullOrEmpty(ConfigData.Pages.Value)) + { + try + { + Output("Validated Setting -- Max Pages => " + Convert.ToInt32(ConfigData.Pages.Value)); + } + catch (Exception) + { + throw new ExceptionWithConfigData("Please enter a numeric maximum number of pages to crawl !", ConfigData); + } + } + else + { + throw new ExceptionWithConfigData("Please enter a maximum number of pages to crawl !", ConfigData); + } + + // Check Latency Setting + if (ConfigData.Latency.Value) + { + Output("\nValidated Setting -- Latency Simulation enabled"); + + // Check Latency Start Setting + if (!string.IsNullOrEmpty(ConfigData.LatencyStart.Value)) + { + try + { + Output("Validated Setting -- Latency Start => " + Convert.ToInt32(ConfigData.LatencyStart.Value)); + } + catch (Exception) + { + throw new ExceptionWithConfigData("Please enter a numeric latency start in ms !", ConfigData); + } + } + else + { + throw new ExceptionWithConfigData("Latency Simulation enabled, Please enter a start latency !", ConfigData); + } + + // Check Latency End Setting + if (!string.IsNullOrEmpty(ConfigData.LatencyEnd.Value)) + { + try + { + Output("Validated Setting -- Latency End => " + Convert.ToInt32(ConfigData.LatencyEnd.Value)); + } + catch (Exception) + { + throw new ExceptionWithConfigData("Please enter a numeric latency end in ms !", ConfigData); + } + } + else + { + throw new ExceptionWithConfigData("Latency Simulation enabled, Please enter a end latency !", ConfigData); + } + } + + // Check Browser Setting + if (ConfigData.Browser.Value) + { + Output("\nValidated Setting -- Browser Simulation enabled"); + + // Check ACCEPT header Setting + if (string.IsNullOrEmpty(ConfigData.HeaderAccept.Value)) + { + throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an ACCEPT header !", ConfigData); + } + else + { + Output("Validated Setting -- ACCEPT (header) => " + ConfigData.HeaderAccept.Value); + } + + // Check ACCEPT-LANG header Setting + if (string.IsNullOrEmpty(ConfigData.HeaderAcceptLang.Value)) + { + throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an ACCEPT-LANG header !", ConfigData); + } + else + { + Output("Validated Setting -- ACCEPT-LANG (header) => " + ConfigData.HeaderAcceptLang.Value); + } + + // Check USER-AGENT header Setting + if (string.IsNullOrEmpty(ConfigData.HeaderUserAgent.Value)) + { + throw new ExceptionWithConfigData("Browser Simulation enabled, Please enter an USER-AGENT header !", ConfigData); + } + else + { + Output("Validated Setting -- USER-AGENT (header) => " + ConfigData.HeaderUserAgent.Value); + } + } + else + { + // Browser simulation must be enabled (otherwhise, this provider will not work due to tracker's security) + throw new ExceptionWithConfigData("Browser Simulation must be enabled for this provider to work, please enable it !", ConfigData); + } + + // Check Dev Cache Settings + if (ConfigData.HardDriveCache.Value) + { + Output("\nValidated Setting -- DEV Hard Drive Cache enabled"); + + // Check if Dev Mode enabled ! + if (!ConfigData.DevMode.Value) + { + throw new ExceptionWithConfigData("Hard Drive is enabled but not in DEV MODE, Please enable DEV MODE !", ConfigData); + } + + // Check Cache Keep Time Setting + if (!string.IsNullOrEmpty(ConfigData.HardDriveCacheKeepTime.Value)) + { + try + { + Output("Validated Setting -- Cache Keep Time (ms) => " + Convert.ToInt32(ConfigData.HardDriveCacheKeepTime.Value)); + } + catch (Exception) + { + throw new ExceptionWithConfigData("Please enter a numeric hard drive keep time in ms !", ConfigData); + } + } + else + { + throw new ExceptionWithConfigData("Hard Drive Cache enabled, Please enter a maximum keep time for cache !", ConfigData); + } + } + else + { + // Delete cache if previously existed + CleanCacheStorage(true); + } + } + } } \ No newline at end of file diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index a50663c5..c0f290a0 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -216,7 +216,7 @@ <Compile Include="Indexers\ImmortalSeed.cs" /> <Compile Include="Indexers\FileList.cs" /> <Compile Include="Indexers\Abstract\AvistazTracker.cs" /> - <Compile Include="Indexers\FrenchADN.cs" /> + <Compile Include="Indexers\FrenchAdn.cs" /> <Compile Include="Indexers\TransmitheNet.cs" /> <Compile Include="Indexers\WiHD.cs" /> <Compile Include="Indexers\XSpeeds.cs" /> @@ -226,7 +226,7 @@ <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataPhxBit.cs" /> <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataBlueTigers.cs" /> <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataAbnormal.cs" /> - <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataFrenchADN.cs" /> + <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataFrenchAdn.cs" /> <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataWiHD.cs" /> <Compile Include="Models\IndexerConfig\ConfigurationDataBasicLoginWithFilter.cs" /> <Compile Include="Models\IndexerConfig\ConfigurationDataAPIKey.cs" /> diff --git a/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs index f41af8e7..518151ee 100644 --- a/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs +++ b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs @@ -1,6 +1,6 @@ namespace Jackett.Models.IndexerConfig.Bespoke { - class ConfigurationDataFrenchADN : ConfigurationData + internal class ConfigurationDataFrenchAdn : ConfigurationData { public DisplayItem CredentialsWarning { get; private set; } public StringItem Username { get; private set; } @@ -16,7 +16,7 @@ public DisplayItem HeadersWarning { get; private set; } public StringItem HeaderAccept { get; private set; } public StringItem HeaderAcceptLang { get; private set; } - public BoolItem HeaderDNT { get; private set; } + public BoolItem HeaderDnt { get; private set; } public BoolItem HeaderUpgradeInsecure { get; private set; } public StringItem HeaderUserAgent { get; private set; } public DisplayItem DevWarning { get; private set; } @@ -24,8 +24,7 @@ public BoolItem HardDriveCache { get; private set; } public StringItem HardDriveCacheKeepTime { get; private set; } - public ConfigurationDataFrenchADN() - : base() + public ConfigurationDataFrenchAdn() { CredentialsWarning = new DisplayItem("<b>Credentials Configuration</b> (<i>Private Tracker</i>),<br /><br /> <ul><li><b>Username</b> is your account name on this tracker.</li><li><b>Password</b> is your password associated to your account name.</li></ul>") { Name = "Credentials" }; Username = new StringItem { Name = "Username (Required)", Value = "" }; @@ -41,7 +40,7 @@ HeadersWarning = new DisplayItem("<b>Browser Headers Configuration</b> (<i>Required if browser simulation enabled</i>),<br /><br /> <ul><li>By filling these fields, <b>Jackett will inject headers</b> with your values <u>to simulate a real browser</u>.</li><li>You can get <b>your browser values</b> here: <a href='https://www.whatismybrowser.com/detect/what-http-headers-is-my-browser-sending' target='blank'>www.whatismybrowser.com</a></li></ul><br /><i><b>Note that</b> some headers are not necessary because they are injected automatically by this provider such as Accept_Encoding, Connection, Host or X-Requested-With</i>") { Name = "Injecting headers" }; HeaderAccept = new StringItem { Name = "Accept", Value = "" }; HeaderAcceptLang = new StringItem { Name = "Accept-Language", Value = "" }; - HeaderDNT = new BoolItem { Name = "DNT", Value = false }; + HeaderDnt = new BoolItem { Name = "DNT", Value = false }; HeaderUpgradeInsecure = new BoolItem { Name = "Upgrade-Insecure-Requests", Value = false }; HeaderUserAgent = new StringItem { Name = "User-Agent", Value = "" }; DevWarning = new DisplayItem("<b>Development Facility</b> (<i>For Developers ONLY</i>),<br /><br /> <ul><li>By enabling development mode, <b>Jackett will bypass his cache</b> and will <u>output debug messages to console</u> instead of his log file.</li><li>By enabling Hard Drive Cache, <b>This provider</b> will <u>save each query answers from tracker</u> in temp directory, in fact this reduce drastically HTTP requests when building a provider at parsing step for example. So, <b> Jackett will search for a cached query answer on hard drive before executing query on tracker side !</b> <i>DEV MODE must be enabled to use it !</li></ul>") { Name = "Development" }; -- GitLab