From 19bc3c7b36e2a936801c332afd6dea229fc8da19 Mon Sep 17 00:00:00 2001 From: JigSawFr <jigsawlimitedfr@gmail.com> Date: Tue, 5 Jan 2016 15:30:50 +0100 Subject: [PATCH] FADN Provider --- src/Jackett/Content/logos/frenchadn.png | Bin 0 -> 25425 bytes src/Jackett/Indexers/FrenchADN.cs | 966 ++++++++++++++++++ src/Jackett/Jackett.csproj | 5 + .../Bespoke/ConfigurationDataFrenchADN.cs | 53 + 4 files changed, 1024 insertions(+) create mode 100644 src/Jackett/Content/logos/frenchadn.png create mode 100644 src/Jackett/Indexers/FrenchADN.cs create mode 100644 src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs diff --git a/src/Jackett/Content/logos/frenchadn.png b/src/Jackett/Content/logos/frenchadn.png new file mode 100644 index 0000000000000000000000000000000000000000..960e0368023781a16f262792c9b75821e03266f7 GIT binary patch literal 25425 zcmeAS@N?(olHy`uVBq!ia0y~yU|7Mxz+l6{#=yYvY0t-p3=9mM1s;*b3=G`DAk4@x zYmNj1gX8_okcg59UmvUF{9L`nl>DSry^7od1`x2ZuP8`N&Q2{+NJ>r5%(GQ`zk9!u zLS~AsQn;zFfp39xYDT6<RZ(him0w75Rd%vvijut@mraFLMQ%ZEYDuC(MQ%=Bu~mhw z64+d;ykaYmu)dN4SV>8?t&$_iLWKz500rm#qErP_JyYFe10zEPb4xu#Q&V$uBOL`J z149#i14Dg7V_ic_D-#nd0|NypP_pAvP*AWbN=dT{a&d!NSCo=wtCUevQedU8UtV6W zS8lAAUzDzIXlZGwZ(yWvWTab^lBQc+nOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FN zr2NtnTO}osMNnH6pcYl+7QijaD~5($a(=FUMPh-zp`L+0HcctXa6JX3MLA%nr6lX8 z=A`DP=9Lud8|oQi)mM>Q;Oh(5iq*Q@T&y~aOI*uJ@arrNsVqp<4@xc0FD*(=buCNH zD^bSg_~MeFR7Bunbp)zWL8)n24JydUFUc>?$j1?YaA};uSdy8B!yzCUoZ3<=GI3}F z$w0J$(-b7CZ7OmLtelHd6HD@oLh|!->>$ZC0OooHXJ>HoRL}^|%uC5HFV?itN7sT# zI0(~HT@p)D?TiczO?3^8bPX&+3{9;JEUk<zv<(cc3=EJAfv7|?!#A}gF(t7i5oU^+ zu7RPhfk}vwv6Z2Tm4OMeDJW{u&B;&6Osgcy6og77Gg6YR((;RP6HDwea}(23^$YUS zZS)a>NGidp*UB@mxFj(zIn~p}t|GTUFEca6%D}+L#K<_sK-b79*;LoS+{9EjDbdhK z*Eq?@(!|2Z%-F!t3>+N3zR21!jCRRSE(K*EPZv8#S(B2VoS9+;Pe@kgW)_BN#wN+S zmZ|2cx`u|Srn-ryrpCG!six*Brb$NTX$F=si;xV%u*knCGd(jeF$YNv$S*0GDORaz z29~DADQ3FnhKZKCNhu~4x``IXhPo+9Mkyx7DX9jg1{N?Yuo#13l}BcANq$i!QhJ0H zQo*T(sG=~5oJ??`RghRzoC?a*wo0%}4KL*&DH}rCfXfD}#AHx$XO|30=&30-`Y56Z zXF>FUtb!=W#Eeejf&fE1Zac~|QuFX-6o^q^Rk#f)&QB{TPb^AxOi#@#Atnz)Ohwg( zZoG{?sCvlBq(MX!Czeqb4T-7drm2ZZpuA*|q-&aJlBk<#oNS?+n3`;oVrF2GY-(u^ zi)N%)ME4>hs=)z_T3#6%See308c2YFRpB-SH9UzZN+G7AYQt@Oaz<iadTNTDKFnt@ z0dy@ijJf3eyt34y5)xvsAh9ShH?<_SsMs#8C_h&rv7jI)GdU5IA@$4hQlPC0csZs} zlCOYN*g`@G$qYLBH6^tulazFd<VtujDGV_`Q=BtREi4j^k}OPh4J^%)bWJTyl5{Q2 zQ!I5;EzAu~%nS`oER0QI>31||B5E8+SxZ6XMsds$*HVFGNRna>SL+R8)Q~PyAtfzp zYX#N<!ff4u+Q+`WRxXfo#SK&%f})*pW5x#DFR?-jbx@NG)Vw6NA`D7RL)MN`ib73K zL9{SIrW4=bgE$UJJ2unvDQ#9lY{6m-!ZujQBLy&0<HZJE$5=V%=j0dp=BK3E8QbV% z6Gk!<Aq{I0JLV;)f*P*HNQQ%25}>$rN=!!;g-PUO;%K*9SSF>KTbdc@rlh1;>KYoF zo9kK{S{mwFBpRj}n46m<8K)&fk{Nna3?hnZBeHk!xz`5Ni?s4D%1q5G0oTQLhBo>b zVo1RNmWu#2wsT5Bb+?_lk%57MfxaP#gla{WM%NmNUn{aSx>khF(7enNJ0qxVXu?Rk zKwVu(90wO9Ca2n&nVNzs9du!I9RZmYsW}lYnYpQX#hLkec7|paHu_*q=rUM!Mj~{Y z86ay!l7MQ2#2nH{3^>g|M1x%1?6_?7!2?L3!6dL0yq=&H1L`nZ84odc)DlQI5J`>( z7m)!4Q8pS}kZ>T991Sia0}7&SG`Jw)KqNUDTto&GMA>L?LBfGZax}Pz3@C`Q(cprF z1Ciuta1j|$5M`sm1qlZt$<g2<GN2&JMuQ6y4n&fp!9`?1L6nUK7bF~rBu9gb$bf<< z8x1Z<I1ovW1{aY51yME{T##@ek{k^#A_EGdY&5ta;Xoug8eBvM6hzr*a6!U>NOCl| zhzux*veDpzgaeV}XmAl3P!MIK!37BiBFWL<A~K*L%0`0=5)MR?qrpXFKtYs^1{Wk8 zh$KgYi^zb2C>sqfNH`Eljs_Qz0R>Su8eEWYAd(ynE+PX8qHHv{AmKnHIT~C<1{6fu zXmCNofk+Z>aB(3{ZAi^au~jNpvbPIYGS^~YU{FZ*2=ZlMs8VHMXlQ0&`1zlKq2VP1 zL#Y7+!>a@a2CEqi4C48d;*Yv9Fetq7ba4!+xb<dk_J-8sq4(?e&VT-<cHZ|n#qZ}X zH_<=2Nk#f6C)*+8gM6t?nj9+^DTuJxJz@4->a^;U;?$Z23i86?KbSnbyeu6IT0Ldh zg{)89*d!aB>TR~^>jt@NXENrMzq|AK#{0W>e-`+?e_35}Tp_gWRQu)7RWEN|uKxOJ z>-CraqgG$f>ioX%``%f;x3~QFpS@Y|;IdqilFMefix<q^7I?w;_LknJyju`4u3ecB zF^w0?a&Nggn&)0N<Jy%e&3bo>?`@En$d}t@*Gmg!*w>o&ZB|MA&S-PqnWOND#9C7) z4x97VCii-ZW4BA@&%2+i=j_gLaZmO$)AL)dwP#rM6?bgk{jpLc|9;>6-Dk7=<L7_B zee#q|bo}&%JOBO3+cUHBP5$@kCr>?le*5Epx0KZW-`cNlot&)x|7v_`fdI>nb*D2- zvc!xfc=*09o$_Pf`D@q27`!yA_R4u_${b&K|GoJk$^PTL1=oHv^gZ6NcW<vi)2dZ3 zO00PG@8)g)sj>Lt3c+jFu8FOCt+aH><(DQ*@4r@^-QC9iK;Zwk8FA~c@3EHam!38G z<dT=tR!$sj&5TYQQ-Vr%-);N9cCD_=aYwI{2?hsBW2XeQKS}6+S+@IZ)R_up;m;e7 zKQ1&rX2U+iN3CL>duZs9!aFwaORP8)1)L0|dOz5#f2hvE^lF!#$-e8aP5*rP{ny!M z_P&RgzntCu_@l)A?<-eztzEmet#12mRSu@EMHS9mul7zn806I=@TAf&a$3e(&C`=s zt-25ueBIP*<+GhrgqDh=Mw*>JEAcrdyLZZ`(#NY;W&H~B+Hx%<^y@A=hk4K2kIk-& zz25%fcK!CBCVy_*h`2h&UT2t+dUK89X`@dz`Oj9pmeN!W4K>Z)e0|3A%FltTR%Knw zxwrY{jn|@10oO~7*J*UMr3H$#|1f!f<B#;u8?~CMmGjI^-hO;-WSDJkBQdX6CZ}@U z=j3ZaBCelx-@Gq9Uly>|H0pJl)93Qd6`$uvZV+sdO#WxWy<*j>#5?b1AH80Bw}yea zv~AnZ1~dD%ZIb!tvp;8MXfKtT^}2Smvt(6jq}l(xd#2VW0=qKpHvGNw=fw5Y)X(|< z8{QbU@gLr=Yrc8jCLPVLMH8n^?A6^~+G@&`{_pOd?0s`*o<DKng2^YtFK>GfOxwS3 z^SSf3fdTiwa~_^>GQ}zO`i^y{&z<wzY+Yg{J0*z4@j!w>hFI={8xDH}o%Y|)7t8+j zTHwyxvOuqgm2!_i?vON=>Xou$d-Qr&Uc%kNIc%F+1$f$>WsYY)ICjCLWXm;P*`*?j zFLE3&l-gnPtR-^&wOMc9zHLj}fB*RBJ<c{i_MJ~L%DlE|^XBH)yF^?aWsdhs8p!bR zf6RY#ux|hT55?Q-w%<Nl8oT9M`?b6?3`;|joo4KN|9eWRr_AxQF>Kd*GX+@=E;Q_W zZ1QPhkercd&aAg(yEpo8-@bje;p2}p40t>zJz3BGi1&=t35Vt>rkfWzFfDyODfMO% z`xl!}I?;74Ht7?WWw7LGUi32Ckfd<zK+{ddp1)bnLDN#(f2FybKlZ)frheJHMCQ0< z$~qC(i*Fptjbn3)7o8{;i#$~~Z*i1jWnkddt^(eDXZbQdXTG0TInR7|_~yqMD_%|d ztD~y#m32S<)TU__p}oqlw|<<pM#thr@sw*TjyCC>*ZedmD|LEWWuH$_4P!w?x%R3_ z%{GfQv{R-9U6txxkh(BoX8Id-PR~Gv=f$5k9lAK{o!7Z<e!5FlPH%E9`0rNk&+ySL zYf{YhrR%b7&S%@rc>kN%d7k)&%UqcYFHQ^3s9x-yESoa3c<YLjCKY`dGW<(Jk~?O{ z{^qM)FSw#IuPjHI<M+<H<~Fy#<o<ha7U$(Dz|uZX`sjz|h8w&Rl}s%ME_QA4*cr#Y zML8hIZ1?O!IbPq{^3IogCI&6qa-{gSOg<}%)8c7|4zKpsdukc1%dz!X<i+(<m&hOU zagty7<n@#Nm*id<J&yJ~cZd5%Ub}b1I_+O;XLvqP-r{1Rx5;CN&1sFrX7hqPR15!@ z9Y5T#?Sh7)z^XKR&l}=$^OR?m&fF@&zJ8TflS0OXZ}Z+R2o#y9!dWOYIaPAq>r3yy z&piKJCDKc#|L0NnbDtd^OgMdYy(`Css-Nl&VlBDTo5klpi?{zRrGL~m>3phz#D)kR zkIPf?wtH{8Rmk4V=s9V^GR;Dn$;&k7JvW@`GsAEBl+=^ozL{NqIYsBRM6a8U822Ze zbQ7tG%QQ1*U3&d>mh71e>tD}WyH@v-iR$^{w!?~<vw}oe_r~>`_)ZKmlHv25)UruO zlCQmYu}E3<p6b^k_SK90m$KOEKY#gr|C#-}xk~ky%{be!sAH4PN1OhG2^T(Gepw=w ze)`Pp=azr#=G&aV8GokY_{YtEKFxgW;^(SqdWiSs>)L-0PRBn}W?$gk?<b?5d*0@0 zuKM}U-~HcR5K>)wsr`w53xlraW!CupMV<ZeRlTb}&XDVRy*>Zqwa4i{TZ4~xxOi&D zME#Vj|FZP_=iORLeM+lz8Xp?g{kxNS@0ys-7UvoFpYYH7AtRsb|Dw&Czt%t^V47;? zEF&2{&C`=qJf$wbyS6s^yV#@L3Zt^gZ>OGLJxP<VHOM1o*IF}K{w0Dl$~dAJmF64o zt(<F<5&XF-!|d4+D<5g++Z7$JH)Q`h_StlQ#O>^D3(uM<pP9cbzj{-f`R&4$U6V}o zXHFB{D&}zV;2W{+-%T8E+ne0GVEAp_+pFu&uTJq?<@D~oON+>zQ`rIwR?pgU_xZ&O zs<UDftt{NGScG?N(V00_bZKyqS8tikqq5n30Z&Sg?wQn4)?dE=X`tQ_Ir+MWO6Pyo zZV6Ml`RVQ{P0gta{O*4aG(WF+<$eD_=HA06Vn5mId!?;A%rt$$2@8uIzKfKq`YcqU zcjsTr4)`)pbHzs6v-<H~CrhmM9(K_Ulwx6O@pd_?9lowWb?T0Fm*?>Q$nF36^|p@5 zi&YP*<`o*BUzz23ntwv*Imc<i-bZ=b`MAQ&9&I^sGW+D(sl6IYAC>85F8nwpX`{!5 zO-j1le0ka_p+Q$>#Xfv+MRoh9In0_XvpdE3imZ-KG~F6<=6Ug!OCg({9jkGRnsOy* zQ@F^LphbdHIBX)7rz*vs(M-DPu%dt^>e`uf&ZmytilTXukF4Z2Y71<Z{KR2hZ8qEV z)EpJg$cY;-OBLJotw?PZ(%YvTuxgPG>ttbNKfCWn=98yreYv2WdgAjYN3DkzJB%Jb zovvRL$>mqp7Bk~ZqzG%ZRzkry-9LM!r~jO;AD?#Xb<i}?s-I68eB(T;cJqHK?~i}9 zbNc#11DOS4l0n=p%o+kt%CEB|qq4S72+~X4am#1f<;>0NjwXFv<~Qlygr;p#x~DcR zx&MChvP|abnX{CZoKCoKP;kM;2U~AU{Qc*daQ%_@dz3iZ3^^QkUsXEI6%}~o=H&Dp z7o<Pk2%CO`yCCG0_x1G?w)THK@Y2jLcH!$O=kLtg6rNob*#7;S`<$-ml1CkjbXIO^ zS#;{hp<d=?jwfEOW!(Nu^7*dhx(8p(F0VOqHt+M%X6AjW(KQdgaQgMLg`}R^dV2kb zsX-HeDlQB*mf*WF`)oka$L8hdHavBT4R2X%X5=3!bLYuTr^1V0J8fL|iwd%MpBLZT z60|g%BdPAg7t3>Pdh^tG)crNJ+duumd!G;8O`BH5vA=c=)Hyq2#o3tMb6)W75a6D4 z>G1X6Z|+{&^4KtNhBDjt&wKyof4I6PB|E#@=CnqLugunqQH>18`7@SI|CvAkhx5P5 z#t+_n5V8M!Be~|q!Q4x0Qf3{yQ~!=9fB!d;`70OR?iP$n{ARJx>zQjeH~;?0`;Gtd z$VV<IX=ilG*kbnWtr6G7*WX^0?N;%0T7Eh6(Z0QL`7h_BO5V+zzH8n64U6_^cGz2S z!~{<jP2ef=T`8j$y<6t<ZySSmrVA&V_(m*|ocjDSU)7&0%+Ftj%xch#7oMeJbJDu6 zG^2fzzWqmw{pFSY56?x`z1vr8b1BpCMaj$GdUlf{=WVk0|FP~x=)V_FyKSt$&bZ!G z-mmuO$~!*xQ|WsuI;XF@`y}_f{TcuHG7mQW`Fee_T)+Kak^ld$@mSgBsY+U3?BJh! z``N-c7yB+r7hRi3<I_TuO4_eKtNwGcSpP%%PUrLd8v?ygN!_$M=5Jrs#&28Ev`#={ zZNTCg#pi2!@9*E~FSxy6ZNl%WEsE>8oQ^+#xR$eTRY2UCjH=1?znA8JocH~YZ`}&! zrCpZg|6ATJUsp6iWrz0iRqLM0{><*Z(sb~e*z@D*A7qankE`o^Z?p4o-h&BOEjQIS z9lx{w)6SzkQTGDRT@~ZeQs-2!d2q0}=JRL&kIyqZm1T|yRwW+ZRjaYN&Z5Y_JRmt( z#j{A`O_GGqrPL0$2MuR-`c0SryN>_U_JTrLMvf@m{wKG(|6H4F{<BEshA#KFO>_3F zTe<acLXo_>RqUP}?bTnq|FoyidzAG%@!v~l{?GdY9bR!fV-NrNXsYy`Z@X=F?e(>P zSM=}D;y*^L`~RIez2`;lWdE#htGY5ni>$x?I1nxOc<uMQ?%{enOris{99P>utUl*g z@#mCrO=xx>J9FK;H<EJiD$XoXJI!D5Y<Bq0S6hDc-HkmIHvPBz?LUwG8a^NS|8Q~j zhi~t=9)+IW!)5XRLHVb@hZS?@7FF(AaD^}MxrKeneY^U9x5R%oSsm+~=09s0?~&R2 zuFX<e-_fMkpWC`*>#=^*&5_5t_4gimcYB}oi$F!oeZ_6Z@8o|x{+oSHU(}VYOEgxq zEx9PQ(W-9E>RE5|PV6hIsGC)sc3pWg*IFgEjm~9{8*VB&Tj()eXn0-m!)cPZ>S1C2 zkHYDCpI&-bpQ+$**SPRt%_3vZO$s0Hx|#P}QuDm@gD2nQ-_K<tTW-JI6ruCt`)?hw zZnc0pDxB-SNqpN~t{~TYL@}$BL$)`lYUP53?Gre&td#A_dgAmq^`wj27u~G>sQu?% z@04qAq^9rDt@||h{j<A|8*T?(@XP;jvHyR1-Qy(IwRZOZWb2#j|Jm|yKB4;8nEykg zw0Xs|D9_w)LXnEqx95Zh+x>j`{}lH-Gy8uRn7`C|Mm}L_mX-TF|Dkc~gX!nyJzUs# zye{%0m&yGfZ~i}-Z~Iee{nzrJt>6E0?EiIQ#%q0>(@&$rEdopzK7Lev?$u<bIG6qN zKCM)@JOBA>$Ns-3m`iLur%f;U6?^V$v`yuNjU2XWd+!-#1y1AH75i$H+62+J8jlYC z(@XXb$aaiB_w&@*>pL#>Rn+ZE2rPHZ{!?*eak^X0yJ_yLZ!2D(l$Xx>@!HAx1u}~) z771_Np=LUBb)mFsm`h?qfy|);&TIC~(PnvhHhkvRGRwQAmJ<~7YhPRc*!f9GY4wVv z_W>N*Jkc$QH&|ZvHcK5ZES8dR74*`)WU_a=KtB8YT>gktvuxJu%n+#15PLbNR=CJX zAVlx8ilV0XszoseC!KW56h0sK=zh&N|DSuan?80X$5?#jxNqO7xmv1KVY1xw`(GLD z{``$TvdblF{+cx$b7viqEHe92Fv-?yrSx(gj+nTgmxTWudwl;vR>`Ev*6KeWtmF&T zyLf8px?rY_hdaZhmu7Fdyy#x_toxPUL?>NNuRQ;5hJH--wD_HqjPLJ0_|&!QRsYPK zW~0v>?K_t4`1WOT&5z*thc_=q&RP5TPxEKlZ`vBMjo~6Ze;)ri>@L6PIoGKw&GQyK z6<@EW|2%Qka-Q(V8!u-#IH<`Sv3dG*_V$U#%T}IdHTODc=s)>~teo8ZXJ3{+uT<>L zSgCr1`{S<9nXgv4T@zY+^waMb);8Xe%CBeLvQp=-nIFEcX72rcRlV`QFMbycvAkSx z=(l_yqo>kc=g(KJ6|Qg${w;8@@4%hQvR86$mK8NvUR;^tJ1OP#1a+o86Jkpz^VffS z{O8(f(aCjJSYBQ1D!6dm*LYG`Ujd892j>2l+|w%+Ti>$ZKlIVB;RvhyqK^Tgtvzmk zK2*!4tntd?ow{n~Gu95Xy4Bv+pEN(Uc-=jzQ8H)J-e%T^83(>)I4$y)%8t}NwMyv| zYp&vis+YatC+CD)8%7uUh-_Q2TGRiHf8k=r!xs&=)$(6242;q@4|phWqUZXv?I+oU zxB2z$P|S|eUT>(az2){J>tn);b~$W3W019*@1ObdH}`j)%lmGkeoMDy!i!zc4)p%G z_x`8A`(oRe1EnHc*<!7G)RX;w#6R^8|1MD?b7@V)t-dtt2-(uK$YX~t|Jdcb!Z9)| zWzw_NpUXJ4e+Maa+}4}F{|C?fil4fF{<JDqM|x^^U5pA^&u1<0tmd!ZkBf7T%nVrY za`uF~wxI#{zt8@%`DKaMtYu$*|6N)eDE4L7MI*hOZ-J^4HC`PQyZh0IRktsB)1C!S ztA7Nxhe@odi{x9UaZ^8g(KoThb<?+TuMOMqWdfr<M?zU^f6cq|6^cDCIeeE+Ggvmo z$wDq}(}B1BoUOGM3pk<-oBPz>94~n+kUED+@jztN?RmRDooIElklVZ9=#|d`jt=Yh zpI$%5(tY)*ig(B4#6Q09QQH#zudBZClGJ&ANgkO!ar37=H<>;4n$+>n95(zcJLX-t zFq!v!;p!>PKA#ufHB`T)SGlw2zScv5t;#VD*{*BDF0CyRyPN#y!8yZyUryQmKG&h} zA?(j=OHr@vI|>S=HGXY|`SRO5Q#fxO{VFDt!lmEQx%k?r8#g2KHq4XWbUCo*(<SXa z+1t<mJdqf8N|diiqkPN#_r@}OJ8OTNtqoi48PmCFMd1{;#e$`=_isDY#$G=g`KPL_ zH>m4WTQ;v$<9pUGi_Drs|0RgHw#;h}5?XoXl-X9hMcnVSUaW3W&==0r5$Si4Tgvew z_=9+S{iOT;&M%IYL@6$gYgaiLprIC>{3oyf!o~$TM<-l*Ra(r%a_C{y#NySfDmT2H zG{yAc3yJ&Js~Ydm_TMSyTJ!zm=Rb#9V^5i=+7`}PA6M7-zv8Rf%i77JT6Y#M4_nr6 z@kE?EYTe<32UY(aUB3K^r&Hjx(=FM@6{CGR*^gUXm%gw_DOBq1k1w}%ofZb(a?w=1 zY_ojAq&+>BwOO<0hOaL&)vb_i;%Ya@x%f>+?}&5Vj~|-b_RQLIrK7B8|96}D_htue zIeyl!{&}><Eyp7<em`GyPygZ0KmVBcq)&bS);F2%@>GuDKX?1Dd9~R8w%mZ^@>#(G z#VI>{+BDud>{(h8bT84w_p<2YE~n5bdHJF*;@(v_bz3jj?>ntC`}2PN_~*aN&ON+p z+5J$n?q&1)<)81E=JEG^PELRK-08ad_gg(TpC`}%&9r~#bVb?c6Kwa0#JNU#oGSY1 zESzyc>U!bC&9!#rKc(JRJuzuKG2!jwKR2xJo%}O<`pGKcx<5a}HZO8H%Ir5yvi{R2 zZ=3056>nPh2(O5rsUI2{G^Jg*xRrt3ciQ<Muj*P?h^B@vntJG~jo0?myUWTdSKL-w zZM3D}qi4mtsogQFP1e0$F!jh+CB2^GCAF@RODiT{-edm9%3p8Ul&LOj6ymOl8r^x= zZ2w7kPdrD|I`=0#MN)hDlC~Y5a^CKzi2t&tf)|rTPb=)-AhGV%EICvE*uP@+w_bmm zoc=3hb@1A!6I6>*Gu@=vdh3%Wxt)1AG3d1OglfKzYMF=Zgw4}G`aLyz@twm){C;V` zA>|8v3!h(l`@M6~ij$k>olO&Q^^<UO3G8@o!Lsf^@$~oGZsjhrI5c_ET)#6*U%J*k zf6ci6#|cdq(|;?E|G4^Fwq@FrAjLluCST3{_vNGgWBza1haP$|DRRV4KTyhH>-u2V zj=t{a)mxn9?-x#3r*PzGVT@bh0rB<wPq{NqpTD<YPSmxE<?;{b	C?G0Vqm$?c?& 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 aa)!LyI>-Mkk#YfDdg1Bn=d#Wzp$PzGSe!=y literal 0 HcmV?d00001 diff --git a/src/Jackett/Indexers/FrenchADN.cs b/src/Jackett/Indexers/FrenchADN.cs new file mode 100644 index 00000000..58238ce2 --- /dev/null +++ b/src/Jackett/Indexers/FrenchADN.cs @@ -0,0 +1,966 @@ +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); + } + } + } +} \ No newline at end of file diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 4bed2f73..8f1d8791 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -207,11 +207,13 @@ <Compile Include="Indexers\ImmortalSeed.cs" /> <Compile Include="Indexers\FileList.cs" /> <Compile Include="Indexers\Abstract\AvistazTracker.cs" /> + <Compile Include="Indexers\FrenchADN.cs" /> <Compile Include="Indexers\WiHD.cs" /> <Compile Include="Indexers\XSpeeds.cs" /> <Compile Include="Models\GitHub\Asset.cs" /> <Compile Include="Models\GitHub\Release.cs" /> <Compile Include="Models\IndexerConfig\Bespoke\ConfigurationDataBlueTigers.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" /> @@ -566,6 +568,9 @@ <Content Include="Content\logos\tvchaosuk.png"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> + <Content Include="Content\logos\frenchadn.png"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> <Content Include="Content\logos\wihd.png"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> diff --git a/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs new file mode 100644 index 00000000..f41af8e7 --- /dev/null +++ b/src/Jackett/Models/IndexerConfig/Bespoke/ConfigurationDataFrenchADN.cs @@ -0,0 +1,53 @@ +namespace Jackett.Models.IndexerConfig.Bespoke +{ + class ConfigurationDataFrenchADN : ConfigurationData + { + public DisplayItem CredentialsWarning { get; private set; } + public StringItem Username { get; private set; } + public StringItem Password { get; private set; } + public DisplayItem PagesWarning { get; private set; } + public StringItem Pages { get; private set; } + public DisplayItem SecurityWarning { get; private set; } + public BoolItem Latency { get; private set; } + public BoolItem Browser { get; private set; } + public DisplayItem LatencyWarning { get; private set; } + public StringItem LatencyStart { get; private set; } + public StringItem LatencyEnd { get; private set; } + 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 HeaderUpgradeInsecure { get; private set; } + public StringItem HeaderUserAgent { get; private set; } + public DisplayItem DevWarning { get; private set; } + public BoolItem DevMode { get; private set; } + public BoolItem HardDriveCache { get; private set; } + public StringItem HardDriveCacheKeepTime { get; private set; } + + public ConfigurationDataFrenchADN() + : base() + { + 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 = "" }; + Password = new StringItem { Name = "Password (Required)", Value = "" }; + PagesWarning = new DisplayItem("<b>Preferences Configuration</b> (<i>Tweak your search settings</i>),<br /><br /> <ul><li><b>Max Pages to Process</b> let you specify how many page (max) Jackett can process when doing a search. Setting a value <b>higher than 4 is dangerous</b> for you account ! (<b>Result of too many requests to tracker...that <u>will be suspect</u></b>).</li></ul>") { Name = "Preferences" }; + Pages = new StringItem { Name = "Max Pages to Process (Required)", Value = "4" }; + SecurityWarning = new DisplayItem("<b>Security Configuration</b> (<i>Read this area carefully !</i>),<br /><br /> <ul><li><b>Latency Simulation</b> will simulate human browsing with Jacket by pausing Jacket for an random time between each request, to fake a real content browsing.</li><li><b>Browser Simulation</b> will simulate a real human browser by injecting additionals headers when doing requests to tracker.<b>You must enable it to use this provider!</b></li></ul>") { Name = "Security" }; + Latency = new BoolItem() { Name = "Latency Simulation (Optional)", Value = false }; + Browser = new BoolItem() { Name = "Browser Simulation (Forced)", Value = true }; + LatencyWarning = new DisplayItem("<b>Latency Configuration</b> (<i>Required if latency simulation enabled</i>),<br /><br/> <ul><li>By filling this range, <b>Jackett will make a random timed pause</b> <u>between requests</u> to tracker <u>to simulate a real browser</u>.</li><li>MilliSeconds <b>only</b></li></ul>") { Name = "Simulate Latency" }; + LatencyStart = new StringItem { Name = "Minimum Latency (ms)", Value = "1589" }; + LatencyEnd = new StringItem { Name = "Maximum Latency (ms)", Value = "3674" }; + 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 }; + 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" }; + DevMode = new BoolItem { Name = "Enable DEV MODE (Developers ONLY)", Value = false }; + HardDriveCache = new BoolItem { Name = "Enable HARD DRIVE CACHE (Developers ONLY)", Value = false }; + HardDriveCacheKeepTime = new StringItem { Name = "Keep Cached files for (ms)", Value = "300000" }; + } + } +} \ No newline at end of file -- GitLab