From 2ef3983049d7dfec2e12134a8fcc62d71a8d2ec1 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 3 Mar 2026 17:53:39 +0100 Subject: [PATCH] =?UTF-8?q?Revert=20to=20v1.5.49=20base=20+=20fix=20"Ausge?= =?UTF-8?q?w=C3=A4hlte=20Downloads=20starten"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore all source files from v1.5.49 (proven stable on both servers) - Add startPackages() IPC method that starts only specified packages - Fix context menu "Ausgewählte Downloads starten" to use startPackages() instead of start() which was starting ALL enabled packages Co-Authored-By: Claude Opus 4.6 --- .../extractor/JBindExtractorMain$1.class | Bin 1693 -> 1710 bytes .../JBindExtractorMain$Backend.class | Bin 2204 -> 2260 bytes .../JBindExtractorMain$ConflictMode.class | Bin 1958 -> 2014 bytes ...JBindExtractorMain$ExtractionRequest.class | Bin 2539 -> 2539 bytes .../JBindExtractorMain$ProgressTracker.class | Bin 1387 -> 1387 bytes ...ExtractorMain$SevenZipArchiveContext.class | Bin 1666 -> 1663 bytes ...ExtractorMain$SevenZipVolumeCallback.class | Bin 3763 -> 3729 bytes ...ExtractorMain$WrongPasswordException.class | Bin 458 -> 458 bytes .../extractor/JBindExtractorMain.class | Bin 20639 -> 19259 bytes .../extractor/JBindExtractorMain.java | 84 +----- src/main/app-controller.ts | 98 +------ src/main/download-manager.ts | 249 ++++++++---------- src/main/extractor.ts | 98 +++---- src/main/main.ts | 47 +--- src/main/mega-web-fallback.ts | 47 ---- src/preload/preload.ts | 3 +- src/renderer/App.tsx | 148 +---------- src/renderer/styles.css | 82 ------ src/shared/ipc.ts | 4 +- src/shared/preload-api.ts | 3 +- src/shared/types.ts | 9 - tests/download-manager.test.ts | 236 ----------------- 22 files changed, 187 insertions(+), 921 deletions(-) diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$1.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$1.class index 9c99eaf749a8dfb599781b2f9298326fd73033c6..fcefae82f2b36f12c39bfd30335e048c1773f902 100644 GIT binary patch delta 821 zcmXw1OHUI~6#i~I?a*N=Eg^`;DzX`K#;D_8!2 zX4bBCVGJY!MiN--f}KCbt?^8WH<|mLSH8L5Ip?GIt2glL?~ii;!^nByfK!AZ!v(i> z*lAgY7uOhMJ6$o#MT1vsKhr4*KKMm6 z%VgKt~iSCb`GFD94r+q_^hxKqSrPB1tl(Nzg;2#9Ex(Sc4YD0D~P8k)%&T#=1* zbXCL+hL$>qR!+_@%^VbX#nj5Wh;D1Zp}0ka83L9n1QZD&_#4nm{w&`raU-1P+HN>o z*fO}5=lXcquIQI>3%9LFVN8}u7S&2l&6l`!$mWEn3`*@%>5{RCXVeRRU>a({ ze4&|JS-#Kpb**AuIM@5-d0pqmbV;pNd6l7Q`D!H9998ddL&OThMC~$Rjkx^p8T>O> zJk-h+{$`I?O@wKxK2Q~$KA*h}03q!_9ckFKFb?`!FiAK?mO>~Hw#MSeU@_$wVk~it z#@H#EjwsHMrrS*jCSjUv2U^J!JMh@1unt|_x$B?MyLg7++8IJ?r?}bNbAmqQ1QCK0 zD9S047qM>;oyqw5hwr#MRd-${!+e>n#_$R5$IrhIYa_j^jYjG~+Ujv97iQ>x9ta+m r5z!UGEV%-e1>wY#`cvjGUyo(5fF-gzDPs$?&loaTrpR-`EIR)I_-UYN delta 788 zcmXw1T~8B16g{)svhA{!f_zq?MT)egr68!FSOf%V`6$u`1NgGk5f{p~ZkI}8qEG$+ zGtVZz`C^PFT8tQsuRiEs@ZJAlyjyXSx#!-wckVrNcHjD5`nJCR`FsLk40$h_G3c1M zV-hM_Fr*@hVFe=!E-4sQ;lYpxV-8+c(SR$CcU6UgaTOD|rlJei6-+9)p&<2QVPKKL zSJjJQi|^}(X_z|<(pH8+a}LW^X~*J~ih?PIrq$CZZ|Q|SZc$CzG`Tff)GHNUVMqy6 zPI(y`CaIB~5-akPUiV|musMS}vNaP;)2y6LmzVW&Uf(Wq;w^s53*$F6Oyib@RPrM)E_O z-fG`9?1wpC<>sbQR*++8p8J;|3Tyn)0k7ENscUmUjzrfPI{y=$H;O!aV3!Z>dvSqo1jRRZ zPu~3+eapvCHjd%hID)rDt)VeqgGNw;FMfnGUt=F2$;rW!=bz9tT@UUhLwuDyIaGsx z@Z=3OZ6t#2G(vVE-%X{2nW$mp^M5C$hb(pgaJhAXHmp(j=TV6x)3D{ O(2Jer!3@Q-2>b=eVViIO diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class index 09518699da0d8717ce0b1ab36b33c596496374f9..41ca622c4afda3cf1d1170888ace9bf531733082 100644 GIT binary patch literal 2260 zcmbVNYg61*7=F&#U9yC5Y2;#QL5f&lfi5ZqTM%Jkv5O1TF3>`~G=#7XgoI`jik(qs z)X~xL59lB8D{d7!GHSm$<8N{t-xI>_DtwWA$jN)&`}1D%=ik5l24DzoH8>&?q6%WD zVi?k$?PS5%{jGJw^7o9SvFEv(?m2F9Hf7rD=@W8JGwrUFrf(VcIzwz%vwVX=&2DPD zTGG<&d@|>`rkx)Z4&n-`Au~iLmlhWohO_7K5?E3c)CxG@HkFy57C!0~ zGzddwMJKU0B;R zbZ=C~C0v%!rl1`!G1Qd-N!$K5MH4`^u)%P>r$T$m(h7yqii;4wN)N(7VE>AOPQ1)e zHD=nTH_p&pX)4sr^e)Ho3Ik+xqenuof&}^)8p;tJOHy5^R2163^C{JD0M{fW6%69l z6K&r!tz_2GHH%<~-dR{!EY-e(MX`3{b%omOIzx@;WSz%`JEav!=V};mrF3Pgq4PLK z$iC)#jxcpY!A*=(+QuVavkDANm1&3g_pZkACMk_P*<6d`Ee2V9QR`AO#xY456pUTN zeq!#FZ>OlHbTPCkA1H+DSJu_e6C&{CE=4mahS*1F-! zm_bHVXqF+;BdU;PXkD`NzF~Q0-f%m@xf!@?xZceUAwRER0kSlnb7tPw zJl`e5)f=v}y?ngIYI7bfwmEGlbSi7QZWIb#BZGrM1n1gbr;A6O93RuIaD;DF04)Z6 z?0i{`lT|tA_^xium;$ku(|h}xXj}#*W7{;3V(}OS2|En^|0gpldf3MkgVYI^=zkKM zxF{AqKqsx~;H0HY998^YfQDAFl8Kf`PQwE*tP#~or+DNdE*zlgIhyy0V$kVbC3t+7 zG^yjq zABs9rV%O}esS;7Qe}w6kBh0KE;+Fc(0p`f&+yUM_#NzKk3?lzdiUa&JH1f~U#=pQ7 z{w2ovSD4^mBg4PJ9RC)1l50R!bw)z>U#O*bYeZ>?;~~rtu{Jsd#<9dtV5h*MQKl&l zMUw$kKuguH;42ZzDYp_Wq9(TzE21uUN}1x@#B8X1kLoO*p(D~&??%+&L)?1`DIo7h z)$xF=3CQp5gtcxKOT$Dnj6oQP20#&OgEc)jtx}WeXPB=h%i#r%Zl196Xw6 F{Rgs(GJ603 literal 2204 zcmbVN-BTM?6#wn!V>inJaSK$iR6vSJfCiNoTd?xcv<(KR4Nybmsjp7T3jH-G>8+aCZX@gRZ_ zUT4d!ggML$NC>ztVE`$%+z@e7!Y!mDSi~FbD&9lG#!B)TF6e`DE6y4J%KVgyHqM%vvhTVd`G1j>DbnqzF7Gup3#9JmkV_1fBjYI4dtLal5t#gQE zjl88gu1&PMPq`Lnm~FdQSg&QgJ?l`Bm(-FMih%bxNUEmk=hAjX*7YCxqdw=mOMs;*I&NZF!mYx)g?736Gl7hYtYCF4cJWW?D}C1ku0Rlp{P z$yP#?k%T7WCD43;5+_5)wt&2h`!HneV3!6-b)BLN69pNA7^aP>hdbm@VuI#`8k|G4 ztnca8L!)%OCF26ahG=VYN5%+KhE4_}ZJK&sHRtTSThJ{hwXf+V$0%A9_6evqJ@c+% zZs|5-Nh#`Ev+}NPI!0c%2mGNNzpLBM)d3k6h-eAh3_Hn|mpOEt*kjcpswW#Y2gFh{ zix#E4E7n-@mCYSpbI9k||4A)}ON}k-y*JYhE#f~3jgA;Ea?{gz&xfU1hXv^sA>vQ7 z^8v^4xQ<~aGeu2htrNVnva(u#i5iZ`OPf>76hEMcP%RU3#P`{VX{1*764oEMs!5N} zQv>OzD89iVtd>gNJ3D?#7|kaAsuCg5Db~EqPS6uRK>r}2*d1Pf`QXiNbmx zi1xAeCq0Y^Mll8sA%Gy+k`trn4k0AQe*yOs=`{}>bPE1cg!xBk<3GbIbi&iWp@>U$j}^Ml z)BgR$Ul0}XN33t5kfiTTtJZz z(f=47o_ap0L_JmU)aOIW8BgsB|4!%tB_Nv%R-j~ap$fXPkHMehSz$HcKSC$}G0yX! cV4SKN@UuOG*T^N0Yj}aAL{vgpz|d^pe_|XdZ~y=R diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ConflictMode.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ConflictMode.class index 3770d2101b57a2b2a42737daa6d1bc7a14e74753..098d94e32c24469dbd0d0f7c49b143147e4cfa9b 100644 GIT binary patch literal 2014 zcmbtUTXWk~5dMzrShA3*E7FFh4Na5K=F-H4wv^T(I2X6Lwc}#vQnx@+A`H>| zQo68Q$SkB8lGzuAObSdzLsVdfi?=ejh2kX*novXv>G|ngTBu&ua0GD%d1z@myO=IA zOx`<0SQR>6krCI>jbjX5rtLbMp?i2F`@q;R61L$~68V(}-1L(wdhn`@6B>GPl9=op zq@AEjlr{`I;Q3XC%flVsv$j#IB|8*F-?N-bvNM7JXyEs0cnzm1<4Mc0{3(Xxolq^$ z%*c|0GYn8Mh#?uTYdDK@4Do$NO-m{z#Ug6|%%=qt#Rx`ajA=NJ@wRv1TXrJrnubkf z2p97C1qQuIzT*>d9YjLI1-wDGs5Cd+QQL?kc!S%xY`ax`HG_$f&I;c!%6P?if`r<6VY<{YqqP zn^z2b+N%Ur?)d3Vldt=h>&Td9=-Ibx@|p=OyUaZmDa?win4$ls=U?j=4)1Bm;3g3<*4MdHW*G0}-^%-7DOw?^q$y-I&jN?%Ii#g{t8MFY-|DVJOpxsSkyQVJC9rnl+2}JIe-3ugUJ~mFQUm`qLi7XV(XmrIIQ7#3@DE8NVGasD+6(d{!lVQ4 zkNyrt?|*{WXdMIF7_MU?tG_u`$E9x}ZzHs&zb%AE(DZA2T6s&K+!NHTT}+jBkt*$A zMo-sqgP?BIacc+jzcoUOOa~}Dq_5B|eT_cp8=R3I;iB{%u1Mb_CH;Uo=`rq;T^x~U zM#kWuh|&9KNQnm@RvSEO!@-7(w8_#7Z*aax&~H=mIM=J NI>op%a8y#ge*=)R_MHF# literal 1958 zcmbtUZEw?76n^Zuv11$vrqEGdT1r=vmXdD37_{s9Lf8UH!GV(I!RCJ@D z;69h1@MT`b2^1CdspLek7ZnW3SP&2^lrr;6^Vx-r0BzQFR&C4l^KONansprJ&Duu2 z&gufjbEaEM*8?+Ht1vs*WNEhPdxq(|Ui$8g7wshPIWwWIQkQoWBHQ;PQ3v-n<`O)!0Wh@F%CQN$|G~Yot;{>&dLvWp>E~-U0@Jx2c;$`VM*!3&CIvU>QV?3mx zge4iD3b^^dCr85w${I#-MF1!}4G*v^<1-DP;|mQB@g)_=-C*8h&+?fD11lQ(FhB!W zU$fS!Vn%&U!$oe1ddx9uK%#AEf(z&}ZP%gF zb|+IEohz(7Vx~{!zx-e7DBybMNQLKkyaTm0zHJ(u7eA{s*u?LjB=+3*X$QV#r*p1p z@JW#73kAAM^d@@8F9JK_yE*qU-6KYwJcsrub^3Is+KLioUj>Fu$NFSv5_^?MJ)mwH z>+8&^2pH{jctcW6(_Z6d)4#Cyx0a^H=#D&3SF<#An$HqIk|rpWG&?||mybAk zbRmXINJ2o0v;mU*QMVy9AihRqi!=elv_|g;oqU?@-xJT8&I|pYe3EEM@AaZ zoN zi*E5doD;vt1@Q-56@SDH@h41)&v09Oj(5o~foLo%&J{Rtl9wsGREx6+QYJ`9&y=Vp-ZoqFC^k0~U?Ohd0!fufNEk=|RPW!N8}Awb diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class index 619418a6ce656d35200e766a89d1a28a52e04e84..f2ccd338a03a7aa6cc9a6da5a03dc3836582427b 100644 GIT binary patch literal 2539 zcmcguT~pIg5IwgogqFxhQBV+2tU?RY77)c?g%(h>2t$z>eQ+jCV-1v4lN9m6f8&h$ z;;18oqci>hf0X0iw6%rChah7gy1B{O-E;PC?&jyOZ$AJ`VL1Q~yaIe8n$XOUP>MTI zv!s-^b84Zqr$*I1%a9eTXhiQNH9fajU9QNQ9;&RgqQ0)aD5<8!&^)W@nw4Pij)XV; z6ud=*9{~nYDe6xPnqsXKb1H+Bc9khr=PdECqKH#yWAIzDkyot+&0uItZ^^rIR4Yc8 zw1R5GIz)7GSgUL(&$L~YKj7|NB2MEBgOHV#ZB@@POuHkLEc@9>dPJPXIpXqMHqDns zBWE&nl)02Ftq@IXrX>Z?#|wR)9@#7lvYwBwX17$u@?!vl0xpQS$SXB-?8-3sGWVC) z{J4aWfXgDTaE(*0;#GuEsQHH*s{%$Cx@^L>lzGFDUvWJF48u#3Gg@Akt&%}o8ag6* zw!F55W-LR~^HM^hxGB2<+S8h@J}B*ERbxZW7RWy!D~f8Gp;#=&us-5$KN?z53Ll|E zsjC^{Jj31zWH?S`>v&9X9&0DS<51t%bWZa4Rnb#?@%jXkxe|To#buaunLjRtH)|3m zViOEcT!v`ujB;aCkBLg3RF}1ElRR6kdbIfZ@2QVpys*0$;z;4EI^tZ^hySdjdRV%Y zP<5kS$P`P4VxQg&J&luSoWB4xW0rMYHBtpShE-EQieb!6y%{1U-Y8lLNfVmxjM1GWymSQ& zL=Mm!`HcR32xNx#5sZAt@S_8S_tE`{5DZu8KL{VCdIO*5Em}SA5cIt7Xu&w5m3aMR zdAz2`7j%F8!2Q6)&W?p z8Z}urFjIjJQbjpbh>Skoha=mYe*ru4l`!iV*@1BjZXw|aZmchHyiF`Un8Q5TA=+0D Zb;AOB=^G-6#kpRZ{Yfm5y+hCG_Zx#)wOjxI literal 2539 zcmcguTT|0e5dKcMq?8~~R0IVSt3pB20)kjXpn#$v44^am;7mx57$~Xcg81N1!U0AG zM`!#2{wT*iX=4kG4^hUK-RwDg`Sy}ce*FCU9l$W=qfjtfN93*`V}jfhBrV8z6b<4w zhWmAB#e|FpQA9B+V@k#}L%n9{Z;Ty2XXH4;xrI$_M^lZwD&#anougSB+?g>f21U=C zuXBd(EatOZMutu@WhW%gnh4yS=iFVc@?FQ$bSH1AkEabY zJL`*!nqkJveNkk2UDLO?nUyg^!gN7k7zyXJXeTmxOR(POtt=sHDx~Q;MowKYY)4$L z8XKnOxE56*e(HL>Am=o5LtS+&!`w(ukV);hVH(Z^Lsd`jbB5{+m4%^k!7%w#cYB>% zYudUfYE;v8Zrkx>GRe?fOxnnsEBu|yZHHl{CtOk$F4DcH5ED8-YDz@iJ%b3Lnpvv5 zk7H2aSUv+BrzX_*KP3G7!mtoNoyP`X<+DnT>_=S>85=;ZU~4dNB-yGE32_UVSpZOl<1e+7d&57;_}) zquM|p-Wo`v;`Ahm@_U?m;c;pL@tEiX7_N9{9jGTwY8jnerITq?(G$>_IEI`!MC1q( zvAQGFB)%c~;u!Tus68MJhHFG)sHRYRsFwCoFC8Ez9X3VKjd;;t2XW!ACh-}y`@hX< z#i6uEsyA>mK(!`F^%i;pRI7tjdjnOgf>aae3-C(m_$m5WsVKNzrYd=99fOss;j2b4 z0JW&vMHLmQa-#3$5dzUB|DYU*H-*7K&jE-d7@|M1LSs+DsH`txyhA7@jNm-+Zt80c YlW3W2r~k7_Tq3RzzfRNBPxnCN7yPERcK`qY diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class index 1623cead68b536309b4ae63874a49e4160de8883..8c1da8f36d621da21b9cd4c61b76568c2756569e 100644 GIT binary patch literal 1387 zcmb7DTW=Fb6#iy>S+BEkf@3fQhlDgK_5v|2cXDaS1+beC6Qf8xp^0}Cme{-2dX2&> zPy7I0Tk#jDnhO_EBoH7#0wh2!zon{r#xaQ?@PMWDIrGh#bH4MS z&~9KG;ta7(GSkApLJTTmi6NBE=QDZ5Bw=7Xb`avbz9>_M2D;P0E`{nuu3DD9w4xNd z+dv9kR8v;$&6>eHWrZ@hb?R#SRHF;q%@#b{@r%AEMP*P!mLa*>AHlY>FjlwAmh_?+Kp=vj z8I0M9%Z1sQv$K=MVvmB47&wY!bSa`*m5#-5Fuk6?ab@dS1~XfrX5vAW;)F_Zk|9N{ ze={(GL6Wmy#Sa@8!7mIE-(AnokVtPmNx4sBOvAW=2~09{e5Gv$t*S&QJEY6kh|e(u z)A@`tNt&60C6>@(!A!mk&5U-hp>-J@roN2i9ol0!NB=w2Nq!eWz6zb+ zL!947l0QHXatJrG^-?uPv0*c?iq6fm>cNBmg%E#4@%fGFH;8SJ;wQTGTfZTA$Gr3> zlHG#pn&i;eaqD-qn93n6fjzpy$=V945YILA+#E5xn$>uNX)k33e~d7H0*yb#HvSA< z{5ksh3ykoW80W9>D^(4GYhA==YJ-24v{3xpK9w}vtEMOD;QeAsFG{F xfleX>{thktJv#UYB>3Oh#s9%^{;@%v6l>mVN0TLiVRrnhSG2^&NQ7R;Cd1gG8 z!nA=I1Fs4sq99Ur0Z|q(YeAz~*O7Jw(sk8}DmtubKO$>MAUW;%UNj>R&zB2t5}ONL zEzq~*`T9z`v8KbTYOPKhQ#ZV*8mW*1V|jKplzT(@HL~@PzP7QfnqA)%`_2bGnbIuo zSB&RN9BEa9Hep!wIPq|K*7Mzk%_vm0C)w(Zqj9SYd1^KVsFS1zwERIB4Q z%6Og05{+Wg#xkxLSg}#T8wOSdrfG1+R=d{TaCN=Csf+p{#^Pbfrz&Bv9_m)>Dz&k} zwfYWe?2_;6aE^A+(k+4h1DGpo*L95s+k2o|R?*uu>4w^*b>_?E0#5q*v&FjQCM!2f~qCkd8IBi`W+8j=6=zEwTxW5gp=4c?WTM8>ajm8F?4O z@*c+Nd$OBtf~pDfO**k1BoAUO(nEfMg#41cC-$qqixlGIKR;&Pd>`>!&b5z`7*kZ& zA%TAJn;#(Uut2&j43qW5Jytac=Pu0mW}MM(HQ7NrL0KWcMpAwQLw<`R@;i*m?=dNV zz>NG6bMhxVK~>|B#wg)AwU56@vtayv4@)}z?!Ox8LbgqJb$57odc?f?J) diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipArchiveContext.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipArchiveContext.class index 58bd4c72fab2a3639fa79bf1b0a60319bb37ad94..603dad76daea868cb8fab512cff5f787a4c59ae0 100644 GIT binary patch delta 737 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 diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class index e5c3d6b1be52b45a5d265b7bf5a6600916dfe597..6826c5e095029799a4bdf95f507bb9ce8b0b1d67 100644 GIT binary patch delta 181 zcmW-bJr06E5QV?p6;LGppn{<>q0~YOZEY;nV_0CLp^%eoxQJ9H79N0GcoE}ZGw*wE zCNI;N#>CC{^#(RXJ{sfE32<<$g__$#D!yy$qm46r-Gz$aR22Dre><0DUV2Qcty&vJ zWIxe`3{NURkKA(e delta 203 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 diff --git a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java index 7ed46ec..413b830 100644 --- a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java +++ b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java @@ -11,7 +11,6 @@ import net.sf.sevenzipjbinding.IInStream; import net.sf.sevenzipjbinding.ISequentialOutStream; import net.sf.sevenzipjbinding.ICryptoGetTextPassword; import net.sf.sevenzipjbinding.PropID; -import net.sf.sevenzipjbinding.ArchiveFormat; import net.sf.sevenzipjbinding.SevenZip; import net.sf.sevenzipjbinding.SevenZipException; import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; @@ -43,8 +42,6 @@ public final class JBindExtractorMain { private static final Pattern NUMBERED_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.zip\\.\\d{3}$"); private static final Pattern OLD_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.z\\d{2,3}$"); private static final Pattern SEVEN_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.7z\\.001$"); - private static final Pattern RAR_MULTIPART_RE = Pattern.compile("(?i).*\\.part\\d+\\.rar$"); - private static final Pattern RAR_OLDSPLIT_RE = Pattern.compile("(?i).*\\.r\\d{2,3}$"); private static volatile boolean sevenZipInitialized = false; private JBindExtractorMain() { @@ -329,79 +326,18 @@ public final class JBindExtractorMain { String effectivePassword = password == null ? "" : password; SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword); - // VolumedArchiveInStream is ONLY for .7z.001 split archives. - // It internally checks for the ".7z.001" suffix and rejects everything else. if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) { VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback); IInArchive archive = SevenZip.openInArchive(null, volumed, callback); return new SevenZipArchiveContext(archive, null, volumed, callback); } - // Multi-part RAR (.part1.rar, .part2.rar or old-style .rar/.r01/.r02): - // The first stream MUST be obtained via the callback so the volume name - // tracker is properly initialized. 7z-JBinding uses getProperty(NAME) - // to compute subsequent volume filenames. - boolean isMultiPartRar = RAR_MULTIPART_RE.matcher(nameLower).matches() - || hasOldStyleRarSplits(archiveFile); - - if (isMultiPartRar) { - IInStream inStream = callback.getStream(archiveFile.getAbsolutePath()); - if (inStream == null) { - throw new IOException("Archiv konnte nicht geoeffnet werden: " + archiveFile.getAbsolutePath()); - } - // Try RAR5 first (modern), then RAR4, then auto-detect - Exception lastError = null; - ArchiveFormat[] rarFormats = { ArchiveFormat.RAR5, ArchiveFormat.RAR, null }; - for (ArchiveFormat fmt : rarFormats) { - try { - inStream.seek(0L, 0); - IInArchive archive = SevenZip.openInArchive(fmt, inStream, callback); - return new SevenZipArchiveContext(archive, null, null, callback); - } catch (Exception e) { - lastError = e; - } - } - callback.close(); - throw lastError != null ? lastError : new IOException("Archiv konnte nicht geoeffnet werden"); - } - - // Single-file archives: open directly with auto-detection RandomAccessFile raf = new RandomAccessFile(archiveFile, "r"); RandomAccessFileInStream stream = new RandomAccessFileInStream(raf); IInArchive archive = SevenZip.openInArchive(null, stream, callback); return new SevenZipArchiveContext(archive, stream, null, callback); } - private static boolean hasOldStyleRarSplits(File archiveFile) { - // Old-style RAR splits: main.rar + main.r01, main.r02, ... - String name = archiveFile.getName(); - if (!name.toLowerCase(Locale.ROOT).endsWith(".rar")) { - return false; - } - File parent = archiveFile.getParentFile(); - if (parent == null || !parent.exists()) { - return false; - } - File[] siblings = parent.listFiles(); - if (siblings == null) { - return false; - } - String stem = name.substring(0, name.length() - 4); - for (File sibling : siblings) { - if (!sibling.isFile()) { - continue; - } - String sibName = sibling.getName(); - if (sibName.length() > stem.length() + 1 && sibName.substring(0, stem.length()).equalsIgnoreCase(stem)) { - String suffix = sibName.substring(stem.length()); - if (RAR_OLDSPLIT_RE.matcher(suffix).matches() || suffix.toLowerCase(Locale.ROOT).matches("\\.r\\d{2,3}")) { - return true; - } - } - } - return false; - } - private static boolean isWrongPassword(ZipException error, boolean encrypted) { if (error == null) { return false; @@ -420,10 +356,7 @@ public final class JBindExtractorMain { if (!encrypted || result == null) { return false; } - // Only DATAERROR reliably indicates wrong password. CRCERROR can also mean - // a genuinely corrupt or incomplete archive, and 7z-JBinding sometimes - // falsely reports encrypted=true for non-encrypted RAR files. - return result == ExtractOperationResult.DATAERROR; + return result == ExtractOperationResult.CRCERROR || result == ExtractOperationResult.DATAERROR; } private static boolean looksLikeWrongPassword(Throwable error, boolean encrypted) { @@ -434,9 +367,7 @@ public final class JBindExtractorMain { if (text.contains("wrong password") || text.contains("falsches passwort")) { return true; } - // Only "data error" suggests wrong password. CRC errors can also mean - // corrupt/incomplete archives, so we don't treat them as password failures. - return encrypted && text.contains("data error"); + return encrypted && (text.contains("crc") || text.contains("data error") || text.contains("checksum")); } private static boolean shouldUseZip4j(File archiveFile) { @@ -840,22 +771,20 @@ public final class JBindExtractorMain { private static final class SevenZipVolumeCallback implements IArchiveOpenCallback, IArchiveOpenVolumeCallback, ICryptoGetTextPassword, Closeable { private final File archiveDir; + private final String firstFileName; private final String password; private final Map openRafs = new HashMap(); - // Must track the LAST opened volume name — 7z-JBinding queries this via - // getProperty(NAME) to compute the next volume filename. - private volatile String currentVolumeName; SevenZipVolumeCallback(File archiveFile, String password) { this.archiveDir = archiveFile.getAbsoluteFile().getParentFile(); - this.currentVolumeName = archiveFile.getName(); + this.firstFileName = archiveFile.getName(); this.password = password == null ? "" : password; } @Override public Object getProperty(PropID propID) { if (propID == PropID.NAME) { - return currentVolumeName; + return firstFileName; } return null; } @@ -874,9 +803,6 @@ public final class JBindExtractorMain { openRafs.put(key, raf); } raf.seek(0L); - // Update current volume name so getProperty(NAME) returns the - // correct value when 7z-JBinding computes the next volume. - currentVolumeName = filename; return new RandomAccessFileInStream(raf); } catch (IOException error) { throw new SevenZipException("Volume konnte nicht geoffnet werden: " + filename, error); diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index a09a13d..c7f41d2 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -1,4 +1,3 @@ -import os from "node:os"; import path from "node:path"; import { app } from "electron"; import { @@ -7,7 +6,6 @@ import { DuplicatePolicy, HistoryEntry, ParsedPackageInput, - ProviderAccountInfo, SessionStats, StartConflictEntry, StartConflictResolutionResult, @@ -25,8 +23,6 @@ import { MegaWebFallback } from "./mega-web-fallback"; import { addHistoryEntry, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeSettings, removeHistoryEntry, saveSession, saveSettings } from "./storage"; import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; import { startDebugServer, stopDebugServer } from "./debug-server"; -import { decryptCredentials, encryptCredentials, SENSITIVE_KEYS } from "./backup-crypto"; -import { compactErrorText } from "./utils"; function sanitizeSettingsPatch(partial: Partial): Partial { const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined); @@ -208,6 +204,10 @@ export class AppController { await this.manager.start(); } + public async startPackages(packageIds: string[]): Promise { + await this.manager.startPackages(packageIds); + } + public stop(): void { this.manager.stop(); } @@ -257,16 +257,9 @@ export class AppController { } public exportBackup(): string { - const settingsCopy = { ...this.settings } as Record; - const sensitiveFields: Record = {}; - for (const key of SENSITIVE_KEYS) { - sensitiveFields[key] = String(settingsCopy[key] ?? ""); - delete settingsCopy[key]; - } - const username = os.userInfo().username; - const credentials = encryptCredentials(sensitiveFields, username); + const settings = this.settings; const session = this.manager.getSession(); - return JSON.stringify({ version: 2, settings: settingsCopy, credentials, session }, null, 2); + return JSON.stringify({ version: 1, settings, session }, null, 2); } public importBackup(json: string): { restored: boolean; message: string } { @@ -279,28 +272,7 @@ export class AppController { if (!parsed || typeof parsed !== "object" || !parsed.settings || !parsed.session) { return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" }; } - - const version = typeof parsed.version === "number" ? parsed.version : 1; - let settingsObj = parsed.settings as Record; - - if (version >= 2) { - const creds = parsed.credentials as { salt: string; iv: string; tag: string; data: string } | undefined; - if (!creds || !creds.salt || !creds.iv || !creds.tag || !creds.data) { - return { restored: false, message: "Backup v2: Verschlüsselte Zugangsdaten fehlen" }; - } - try { - const username = os.userInfo().username; - const decrypted = decryptCredentials(creds, username); - settingsObj = { ...settingsObj, ...decrypted }; - } catch { - return { - restored: false, - message: "Entschlüsselung fehlgeschlagen. Das Backup wurde mit einem anderen Benutzer erstellt." - }; - } - } - - const restoredSettings = normalizeSettings(settingsObj as AppSettings); + const restoredSettings = normalizeSettings(parsed.settings as AppSettings); this.settings = restoredSettings; saveSettings(this.storagePaths, this.settings); this.manager.setSettings(this.settings); @@ -329,62 +301,6 @@ export class AppController { removeHistoryEntry(this.storagePaths, entryId); } - public async checkMegaAccount(): Promise { - return this.megaWebFallback.getAccountInfo(); - } - - public async checkRealDebridAccount(): Promise { - try { - const response = await fetch("https://api.real-debrid.com/rest/1.0/user", { - headers: { Authorization: `Bearer ${this.settings.token}` } - }); - if (!response.ok) { - const text = await response.text().catch(() => ""); - return { provider: "realdebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: `HTTP ${response.status}: ${compactErrorText(text)}` }; - } - const data = await response.json() as Record; - const username = String(data.username ?? ""); - const type = String(data.type ?? ""); - const expiration = data.expiration ? new Date(String(data.expiration)) : null; - const daysRemaining = expiration ? Math.max(0, Math.round((expiration.getTime() - Date.now()) / 86400000)) : null; - const points = typeof data.points === "number" ? data.points : null; - return { provider: "realdebrid", username, accountType: type === "premium" ? "Premium" : type, daysRemaining, loyaltyPoints: points as number | null }; - } catch (err) { - return { provider: "realdebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: compactErrorText(err) }; - } - } - - public async checkAllDebridAccount(): Promise { - try { - const response = await fetch("https://api.alldebrid.com/v4/user", { - headers: { Authorization: `Bearer ${this.settings.allDebridToken}` } - }); - if (!response.ok) { - const text = await response.text().catch(() => ""); - return { provider: "alldebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: `HTTP ${response.status}: ${compactErrorText(text)}` }; - } - const data = await response.json() as Record; - const userData = (data.data as Record | undefined)?.user as Record | undefined; - if (!userData) { - return { provider: "alldebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Ungültige API-Antwort" }; - } - const username = String(userData.username ?? ""); - const isPremium = Boolean(userData.isPremium); - const premiumUntil = typeof userData.premiumUntil === "number" ? userData.premiumUntil : 0; - const daysRemaining = premiumUntil > 0 ? Math.max(0, Math.round((premiumUntil * 1000 - Date.now()) / 86400000)) : null; - return { provider: "alldebrid", username, accountType: isPremium ? "Premium" : "Free", daysRemaining, loyaltyPoints: null }; - } catch (err) { - return { provider: "alldebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: compactErrorText(err) }; - } - } - - public async checkBestDebridAccount(): Promise { - if (!this.settings.bestToken.trim()) { - return { provider: "bestdebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Kein Token konfiguriert" }; - } - return { provider: "bestdebrid", username: "(Token konfiguriert)", accountType: "Konfiguriert", daysRemaining: null, loyaltyPoints: null }; - } - public addToHistory(entry: HistoryEntry): void { addHistoryEntry(this.storagePaths, entry); } diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 1d12c5b..6e2b98f 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -218,35 +218,6 @@ function isArchiveLikePath(filePath: string): boolean { return /\.(?:part\d+\.rar|rar|r\d{2,3}|zip(?:\.\d+)?|z\d{1,3}|7z(?:\.\d+)?)$/i.test(lower); } -const ITEM_RECOVERY_MIN_BYTES = 10 * 1024; -const ARCHIVE_RECOVERY_MIN_RATIO = 0.995; -const ARCHIVE_RECOVERY_MAX_SLACK_BYTES = 4 * 1024 * 1024; -const FILE_RECOVERY_MIN_RATIO = 0.98; -const FILE_RECOVERY_MAX_SLACK_BYTES = 8 * 1024 * 1024; - -function recoveryExpectedMinSize(filePath: string, totalBytes: number | null | undefined): number { - const knownTotal = Number(totalBytes || 0); - if (!Number.isFinite(knownTotal) || knownTotal <= 0) { - return ITEM_RECOVERY_MIN_BYTES; - } - - const archiveLike = isArchiveLikePath(filePath); - const minRatio = archiveLike ? ARCHIVE_RECOVERY_MIN_RATIO : FILE_RECOVERY_MIN_RATIO; - const maxSlack = archiveLike ? ARCHIVE_RECOVERY_MAX_SLACK_BYTES : FILE_RECOVERY_MAX_SLACK_BYTES; - const ratioBased = Math.floor(knownTotal * minRatio); - const slackBased = Math.max(0, Math.floor(knownTotal) - maxSlack); - return Math.max(ITEM_RECOVERY_MIN_BYTES, Math.max(ratioBased, slackBased)); -} - -function isRecoveredFileSizeSufficient(item: Pick, fileSize: number): boolean { - if (!Number.isFinite(fileSize) || fileSize <= 0) { - return false; - } - const candidatePath = String(item.targetPath || item.fileName || ""); - const minSize = recoveryExpectedMinSize(candidatePath, item.totalBytes); - return fileSize >= minSize; -} - function isFetchFailure(errorText: string): boolean { const text = String(errorText || "").toLowerCase(); return text.includes("fetch failed") || text.includes("socket hang up") || text.includes("econnreset") || text.includes("network error"); @@ -2308,6 +2279,92 @@ export class DownloadManager extends EventEmitter { }); } + public async startPackages(packageIds: string[]): Promise { + const targetSet = new Set(packageIds); + + // Enable specified packages if disabled + for (const pkgId of targetSet) { + const pkg = this.session.packages[pkgId]; + if (pkg && !pkg.enabled) { + pkg.enabled = true; + } + } + + // Recover stopped items in specified packages + for (const item of Object.values(this.session.items)) { + if (!targetSet.has(item.packageId)) continue; + if (item.status === "cancelled" && item.fullStatus === "Gestoppt") { + const pkg = this.session.packages[item.packageId]; + if (pkg && !pkg.cancelled && pkg.enabled) { + item.status = "queued"; + item.fullStatus = "Wartet"; + item.lastError = ""; + item.speedBps = 0; + item.updatedAt = nowMs(); + } + } + } + + // If already running, the scheduler will pick up newly enabled items + if (this.session.running) { + // Add new items to runItemIds so the scheduler processes them + for (const item of Object.values(this.session.items)) { + if (!targetSet.has(item.packageId)) continue; + if (item.status === "queued" || item.status === "reconnect_wait") { + this.runItemIds.add(item.id); + this.runPackageIds.add(item.packageId); + } + } + this.persistSoon(); + this.emitState(true); + return; + } + + // Not running: start with only items from specified packages + const runItems = Object.values(this.session.items) + .filter((item) => { + if (!targetSet.has(item.packageId)) return false; + if (item.status !== "queued" && item.status !== "reconnect_wait") return false; + const pkg = this.session.packages[item.packageId]; + return Boolean(pkg && !pkg.cancelled && pkg.enabled); + }); + if (runItems.length === 0) { + this.persistSoon(); + this.emitState(true); + return; + } + this.runItemIds = new Set(runItems.map((item) => item.id)); + this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.runOutcomes.clear(); + this.runCompletedPackages.clear(); + this.retryAfterByItem.clear(); + this.session.running = true; + this.session.paused = false; + this.session.runStartedAt = nowMs(); + this.session.totalDownloadedBytes = 0; + this.session.summaryText = ""; + this.session.reconnectUntil = 0; + this.session.reconnectReason = ""; + this.speedEvents = []; + this.speedBytesLastWindow = 0; + this.speedBytesPerPackage.clear(); + this.speedEventsHead = 0; + this.lastGlobalProgressBytes = 0; + this.lastGlobalProgressAt = nowMs(); + this.summary = null; + this.nonResumableActive = 0; + this.persistSoon(); + this.emitState(true); + logger.info(`Start (nur Pakete: ${packageIds.length}): ${runItems.length} Items`); + void this.ensureScheduler().catch((error) => { + logger.error(`Scheduler abgestürzt: ${compactErrorText(error)}`); + this.session.running = false; + this.session.paused = false; + this.persistSoon(); + this.emitState(true); + }); + } + public async start(): Promise { if (this.session.running) { return; @@ -4978,7 +5035,6 @@ export class DownloadManager extends EventEmitter { } const completedPaths = new Set(); - const completedItemsByPath = new Map(); const pendingPaths = new Set(); for (const itemId of pkg.itemIds) { const item = this.session.items[itemId]; @@ -4986,9 +5042,7 @@ export class DownloadManager extends EventEmitter { continue; } if (item.status === "completed" && item.targetPath) { - const key = pathKey(item.targetPath); - completedPaths.add(key); - completedItemsByPath.set(key, item); + completedPaths.add(pathKey(item.targetPath)); } else if (item.targetPath) { pendingPaths.add(pathKey(item.targetPath)); } @@ -5024,82 +5078,12 @@ export class DownloadManager extends EventEmitter { const partsOnDisk = collectArchiveCleanupTargets(candidate, dirFiles); const allPartsCompleted = partsOnDisk.every((part) => completedPaths.has(pathKey(part))); if (allPartsCompleted) { - let allPartsLikelyComplete = true; - for (const part of partsOnDisk) { - const completedItem = completedItemsByPath.get(pathKey(part)); - if (!completedItem) { - continue; - } - try { - const stat = fs.statSync(part); - if (isRecoveredFileSizeSufficient(completedItem, stat.size)) { - continue; - } - const minSize = recoveryExpectedMinSize(completedItem.targetPath || completedItem.fileName, completedItem.totalBytes); - logger.info(`Hybrid-Extract: ${path.basename(candidate)} übersprungen – ${path.basename(part)} zu klein (${humanSize(stat.size)}, erwartet mind. ${humanSize(minSize)})`); - allPartsLikelyComplete = false; - break; - } catch { - allPartsLikelyComplete = false; - break; - } - } - if (!allPartsLikelyComplete) { - continue; - } - - const candidateBase = path.basename(candidate).toLowerCase(); - - // For multi-part archives (.part1.rar), check if parts of THIS SPECIFIC archive - // are still pending. We match by archive prefix so E01 parts don't block E02. - const multiMatch = candidateBase.match(/^(.*)\.part0*1\.rar$/i); - if (multiMatch) { - const prefix = multiMatch[1].toLowerCase(); - const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const partPattern = new RegExp(`^${escapedPrefix}\\.part\\d+\\.rar$`, "i"); - const hasRelatedPending = pkg.itemIds.some((itemId) => { - const item = this.session.items[itemId]; - if (!item || item.status === "completed" || item.status === "failed" || item.status === "cancelled") { - return false; - } - // Check fileName (set early from link URL) - if (item.fileName && partPattern.test(item.fileName)) { - return true; - } - // Check targetPath basename (set when download starts) - if (item.targetPath && partPattern.test(path.basename(item.targetPath))) { - return true; - } - // Item has no identity at all — might be an unresolved part, be conservative - if (!item.fileName && !item.targetPath) { - return true; - } - return false; - }); - if (hasRelatedPending) { - logger.info(`Hybrid-Extract: ${path.basename(candidate)} übersprungen – zugehörige Parts noch ausstehend`); - continue; - } - } - const hasUnstartedParts = [...pendingPaths].some((pendingPath) => { const pendingName = path.basename(pendingPath).toLowerCase(); - return this.looksLikeArchivePart(pendingName, candidateBase); + const candidateStem = path.basename(candidate).toLowerCase(); + return this.looksLikeArchivePart(pendingName, candidateStem); }); - // Also check items without targetPath (queued items that only have fileName) - const hasMatchingPendingItems = pkg.itemIds.some((itemId) => { - const item = this.session.items[itemId]; - if (!item || item.status === "completed" || item.status === "failed" || item.status === "cancelled") { - return false; - } - if (item.fileName && !item.targetPath) { - if (this.looksLikeArchivePart(item.fileName.toLowerCase(), candidateBase)) { - return true; - } - } - return false; - }); - if (hasUnstartedParts || hasMatchingPendingItems) { + if (hasUnstartedParts) { continue; } ready.add(pathKey(candidate)); @@ -5109,11 +5093,6 @@ export class DownloadManager extends EventEmitter { // Disk-fallback: if all parts exist on disk but some items lack "completed" status, // allow extraction if none of those parts are actively downloading/validating. // This handles items that finished downloading but whose status was not updated. - // Skip disk-fallback entirely for multi-part archives — only allPartsCompleted should handle those. - const isMultiPart = /\.part0*1\.rar$/i.test(path.basename(candidate)); - if (isMultiPart) { - continue; - } const missingParts = partsOnDisk.filter((part) => !completedPaths.has(pathKey(part))); let allMissingExistOnDisk = true; for (const part of missingParts) { @@ -5138,22 +5117,6 @@ export class DownloadManager extends EventEmitter { if (anyActivelyProcessing) { continue; } - // Also check fileName for items without targetPath (queued/downloading items) - const candidateBaseFb = path.basename(candidate).toLowerCase(); - const hasMatchingPendingFb = pkg.itemIds.some((itemId) => { - const item = this.session.items[itemId]; - if (!item || item.status === "completed" || item.status === "failed" || item.status === "cancelled") { - return false; - } - const nameToCheck = item.fileName?.toLowerCase() || (item.targetPath ? path.basename(item.targetPath).toLowerCase() : ""); - if (!nameToCheck) { - return false; - } - return nameToCheck === candidateBaseFb || this.looksLikeArchivePart(nameToCheck, candidateBaseFb); - }); - if (hasMatchingPendingFb) { - continue; - } logger.info(`Hybrid-Extract Disk-Fallback: ${path.basename(candidate)} (${missingParts.length} Part(s) auf Disk ohne completed-Status)`); ready.add(pathKey(candidate)); } @@ -5281,9 +5244,17 @@ export class DownloadManager extends EventEmitter { if (progress.phase === "done") { return; } - // Track only currently active archive items; final statuses are set - // after extraction result is known. + // When a new archive starts, mark the previous archive's items as done if (progress.archiveName && progress.archiveName !== lastHybridArchiveName) { + if (lastHybridArchiveName && currentArchiveItems.length > 0) { + const doneAt = nowMs(); + for (const entry of currentArchiveItems) { + if (!isExtractedLabel(entry.fullStatus)) { + entry.fullStatus = "Entpackt - Done"; + entry.updatedAt = doneAt; + } + } + } lastHybridArchiveName = progress.archiveName; const resolved = resolveArchiveItems(progress.archiveName); currentArchiveItems = resolved; @@ -5358,8 +5329,12 @@ export class DownloadManager extends EventEmitter { } try { const stat = fs.statSync(item.targetPath); - const minSize = recoveryExpectedMinSize(item.targetPath || item.fileName, item.totalBytes); - if (isRecoveredFileSizeSufficient(item, stat.size)) { + // Require file to be either ≥50% of expected size or at least 10 KB to avoid + // recovering tiny error-response files (e.g. 9-byte "Forbidden" pages). + const minSize = item.totalBytes && item.totalBytes > 0 + ? Math.max(10240, Math.floor(item.totalBytes * 0.5)) + : 10240; + if (stat.size >= minSize) { logger.info(`Item-Recovery: ${item.fileName} war "${item.status}" aber Datei existiert (${humanSize(stat.size)}), setze auf completed`); item.status = "completed"; item.fullStatus = this.settings.autoExtract ? "Entpacken - Ausstehend" : `Fertig (${humanSize(stat.size)})`; @@ -5493,9 +5468,17 @@ export class DownloadManager extends EventEmitter { signal: extractAbortController.signal, packageId, onProgress: (progress) => { - // Track only currently active archive items; final statuses are set - // after extraction result is known. + // When a new archive starts, mark the previous archive's items as done if (progress.archiveName && progress.archiveName !== lastExtractArchiveName) { + if (lastExtractArchiveName && currentArchiveItems.length > 0) { + const doneAt = nowMs(); + for (const entry of currentArchiveItems) { + if (!isExtractedLabel(entry.fullStatus)) { + entry.fullStatus = "Entpackt - Done"; + entry.updatedAt = doneAt; + } + } + } lastExtractArchiveName = progress.archiveName; currentArchiveItems = resolveArchiveItems(progress.archiveName); } diff --git a/src/main/extractor.ts b/src/main/extractor.ts index ca10774..b02e2a8 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -499,7 +499,7 @@ function extractorThreadSwitch(hybridMode = false): string { return `-mt${threadCount}`; } -function lowerExtractProcessPriority(childPid: number | undefined, label = ""): void { +function lowerExtractProcessPriority(childPid: number | undefined): void { if (process.platform !== "win32") { return; } @@ -511,9 +511,6 @@ function lowerExtractProcessPriority(childPid: number | undefined, label = ""): // IDLE_PRIORITY_CLASS: lowers CPU scheduling priority so extraction // doesn't starve other processes. I/O priority stays Normal (like JDownloader 2). os.setPriority(pid, os.constants.priority.PRIORITY_LOW); - if (label) { - logger.info(`Prozess-Priorität: CPU=Idle, I/O=Normal (PID ${pid}, ${label})`); - } } catch { // ignore: priority lowering is best-effort } @@ -583,7 +580,7 @@ function runExtractCommand( let settled = false; let output = ""; const child = spawn(command, args, { windowsHide: true }); - lowerExtractProcessPriority(child.pid, `legacy/${path.basename(command).replace(/\.exe$/i, "")}`); + lowerExtractProcessPriority(child.pid); let timeoutId: NodeJS.Timeout | null = null; let timedOutByWatchdog = false; let abortedBySignal = false; @@ -900,7 +897,7 @@ function runJvmExtractCommand( let stderrBuffer = ""; const child = spawn(layout.javaCommand, args, { windowsHide: true }); - lowerExtractProcessPriority(child.pid, "7zjbinding/single-thread"); + lowerExtractProcessPriority(child.pid); const flushLines = (rawChunk: string, fromStdErr = false): void => { if (!rawChunk) { @@ -1169,63 +1166,38 @@ async function runExternalExtract( } logger.warn(`JVM-Extractor nicht verfügbar, nutze Legacy-Extractor: ${path.basename(archivePath)}`); } else { - if (hybridMode) { - try { - const archiveStat = await fs.promises.stat(archivePath); - logger.info(`Hybrid-Extract JVM: ${path.basename(archivePath)} (${(archiveStat.size / 1048576).toFixed(1)} MB)`); - } catch (statErr) { - logger.warn(`Hybrid-Extract JVM: Archiv nicht zugreifbar: ${path.basename(archivePath)} — ${String(statErr)}`); - } - } logger.info(`JVM-Extractor aktiv (${layout.rootDir}): ${path.basename(archivePath)}`); - const maxJvmAttempts = hybridMode ? 2 : 1; - for (let jvmAttempt = 1; jvmAttempt <= maxJvmAttempts; jvmAttempt++) { - const jvmResult = await runJvmExtractCommand( - layout, - archivePath, - targetDir, - conflictMode, - passwordCandidates, - onArchiveProgress, - signal, - timeoutMs - ); + const jvmResult = await runJvmExtractCommand( + layout, + archivePath, + targetDir, + conflictMode, + passwordCandidates, + onArchiveProgress, + signal, + timeoutMs + ); - if (jvmResult.ok) { - if (jvmAttempt > 1) { - logger.info(`JVM-Extractor Retry #${jvmAttempt - 1} erfolgreich: ${path.basename(archivePath)}`); - } - logger.info(`Entpackt via ${jvmResult.backend || "jvm"} [CPU=Idle, I/O=Normal, single-thread]: ${path.basename(archivePath)}`); - return jvmResult.usedPassword; - } - if (jvmResult.aborted) { - throw new Error("aborted:extract"); - } - if (jvmResult.timedOut) { - throw new Error(jvmResult.errorText || `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s`); - } + if (jvmResult.ok) { + logger.info(`Entpackt via ${jvmResult.backend || "jvm"}: ${path.basename(archivePath)}`); + return jvmResult.usedPassword; + } + if (jvmResult.aborted) { + throw new Error("aborted:extract"); + } + if (jvmResult.timedOut) { + throw new Error(jvmResult.errorText || `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s`); + } - jvmFailureReason = jvmResult.errorText || "JVM-Extractor fehlgeschlagen"; - - // In hybrid mode, retry once on "codecs" / "can't be opened" errors — - // these can be caused by transient Windows file locks right after download completion. - const isTransientOpen = jvmFailureReason.includes("codecs") || jvmFailureReason.includes("can't be opened"); - if (hybridMode && isTransientOpen && jvmAttempt < maxJvmAttempts) { - logger.warn(`JVM-Extractor Hybrid-Retry: ${jvmFailureReason} — warte 3s vor Versuch #${jvmAttempt + 1}: ${path.basename(archivePath)}`); - await new Promise((r) => setTimeout(r, 3000)); - continue; - } - - const isUnsupportedMethod = jvmFailureReason.includes("UNSUPPORTEDMETHOD"); - if (backendMode === "jvm" && !isUnsupportedMethod) { - throw new Error(jvmFailureReason); - } - if (isUnsupportedMethod) { - logger.warn(`JVM-Extractor: Komprimierungsmethode nicht unterstützt, fallback auf Legacy: ${path.basename(archivePath)}`); - } else { - logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`); - } - break; + jvmFailureReason = jvmResult.errorText || "JVM-Extractor fehlgeschlagen"; + const isUnsupportedMethod = jvmFailureReason.includes("UNSUPPORTEDMETHOD"); + if (backendMode === "jvm" && !isUnsupportedMethod) { + throw new Error(jvmFailureReason); + } + if (isUnsupportedMethod) { + logger.warn(`JVM-Extractor: Komprimierungsmethode nicht unterstützt, fallback auf Legacy: ${path.basename(archivePath)}`); + } else { + logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`); } } } @@ -1247,12 +1219,10 @@ async function runExternalExtract( hybridMode ); const extractorName = path.basename(command).replace(/\.exe$/i, ""); - const threadInfo = extractorThreadSwitch(hybridMode); - const modeLabel = hybridMode ? "hybrid" : "normal"; if (jvmFailureReason) { - logger.info(`Entpackt via legacy/${extractorName} [CPU=Idle, I/O=Normal, ${threadInfo}, ${modeLabel}] (nach JVM-Fehler): ${path.basename(archivePath)}`); + logger.info(`Entpackt via legacy/${extractorName} (nach JVM-Fehler): ${path.basename(archivePath)}`); } else { - logger.info(`Entpackt via legacy/${extractorName} [CPU=Idle, I/O=Normal, ${threadInfo}, ${modeLabel}]: ${path.basename(archivePath)}`); + logger.info(`Entpackt via legacy/${extractorName}: ${path.basename(archivePath)}`); } return password; } finally { diff --git a/src/main/main.ts b/src/main/main.ts index 89a9aa2..9b948f8 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -254,31 +254,9 @@ function registerIpcHandlers(): void { return false; } }); - ipcMain.handle(IPC_CHANNELS.UPDATE_SETTINGS, async (_event: IpcMainInvokeEvent, partial: Partial) => { - const validated = validatePlainObject(partial ?? {}, "partial") as Partial; - const oldSettings = controller.getSettings(); - const dirKeys = ["outputDir", "extractDir", "mkvLibraryDir"] as const; - for (const key of dirKeys) { - const newVal = validated[key]; - if (typeof newVal === "string" && newVal.trim() && newVal !== oldSettings[key]) { - if (!fs.existsSync(newVal)) { - const msgOpts = { - type: "question" as const, - buttons: ["Ja", "Nein"], - defaultId: 0, - title: "Ordner erstellen?", - message: `Der Ordner existiert nicht:\n${newVal}\n\nSoll er erstellt werden?` - }; - const { response } = mainWindow - ? await dialog.showMessageBox(mainWindow, msgOpts) - : await dialog.showMessageBox(msgOpts); - if (response === 0) { - fs.mkdirSync(newVal, { recursive: true }); - } - } - } - } - const result = controller.updateSettings(validated); + ipcMain.handle(IPC_CHANNELS.UPDATE_SETTINGS, (_event: IpcMainInvokeEvent, partial: Partial) => { + const validated = validatePlainObject(partial ?? {}, "partial"); + const result = controller.updateSettings(validated as Partial); updateClipboardWatcher(); updateTray(); return result; @@ -310,6 +288,10 @@ function registerIpcHandlers(): void { }); ipcMain.handle(IPC_CHANNELS.CLEAR_ALL, () => controller.clearAll()); ipcMain.handle(IPC_CHANNELS.START, () => controller.start()); + ipcMain.handle(IPC_CHANNELS.START_PACKAGES, (_event: IpcMainInvokeEvent, packageIds: string[]) => { + if (!Array.isArray(packageIds)) throw new Error("packageIds muss ein Array sein"); + return controller.startPackages(packageIds); + }); ipcMain.handle(IPC_CHANNELS.STOP, () => controller.stop()); ipcMain.handle(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause()); ipcMain.handle(IPC_CHANNELS.CANCEL_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => { @@ -384,21 +366,6 @@ function registerIpcHandlers(): void { const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options); return result.canceled ? [] : result.filePaths; }); - ipcMain.handle(IPC_CHANNELS.CHECK_ACCOUNT, async (_event: IpcMainInvokeEvent, provider: string) => { - validateString(provider, "provider"); - switch (provider) { - case "realdebrid": - return controller.checkRealDebridAccount(); - case "megadebrid": - return controller.checkMegaAccount(); - case "bestdebrid": - return controller.checkBestDebridAccount(); - case "alldebrid": - return controller.checkAllDebridAccount(); - default: - return { provider, username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Nicht unterstützt" }; - } - }); ipcMain.handle(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats()); ipcMain.handle(IPC_CHANNELS.RESTART, () => { diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts index 8df9778..f9c1b04 100644 --- a/src/main/mega-web-fallback.ts +++ b/src/main/mega-web-fallback.ts @@ -1,4 +1,3 @@ -import { ProviderAccountInfo } from "../shared/types"; import { UnrestrictedLink } from "./realdebrid"; import { compactErrorText, filenameFromUrl, sleep } from "./utils"; @@ -16,7 +15,6 @@ const LOGIN_URL = "https://www.mega-debrid.eu/index.php?form=login"; const DEBRID_URL = "https://www.mega-debrid.eu/index.php?form=debrid"; const DEBRID_AJAX_URL = "https://www.mega-debrid.eu/index.php?ajax=debrid&json"; const DEBRID_REFERER = "https://www.mega-debrid.eu/index.php?page=debrideur&lang=de"; -const PROFILE_URL = "https://www.mega-debrid.eu/index.php?page=profil"; function normalizeLink(link: string): string { return link.trim().toLowerCase(); @@ -266,51 +264,6 @@ export class MegaWebFallback { }, signal); } - public async getAccountInfo(): Promise { - return this.runExclusive(async () => { - const creds = this.getCredentials(); - if (!creds.login.trim() || !creds.password.trim()) { - return { provider: "megadebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Login/Passwort nicht konfiguriert" }; - } - - try { - if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) { - await this.login(creds.login, creds.password); - } - - const res = await fetch(PROFILE_URL, { - method: "GET", - headers: { - "User-Agent": "Mozilla/5.0", - Cookie: this.cookie, - Referer: DEBRID_REFERER - }, - signal: AbortSignal.timeout(30000) - }); - const html = await res.text(); - - const usernameMatch = html.match(/]*id=["']user_link["'][^>]*>([^<]+)<\/span>/i); - const username = usernameMatch?.[1]?.trim() || ""; - - const typeMatch = html.match(/(Premiumuser|Freeuser)\s*-\s*(\d+)\s*Tag/i); - const accountType = typeMatch?.[1] || "Unbekannt"; - const daysRemaining = typeMatch?.[2] ? parseInt(typeMatch[2], 10) : null; - - const pointsMatch = html.match(/(\d+)\s*Treuepunkte/i); - const loyaltyPoints = pointsMatch?.[1] ? parseInt(pointsMatch[1], 10) : null; - - if (!username && !typeMatch) { - this.cookie = ""; - return { provider: "megadebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Profil konnte nicht gelesen werden (Session ungültig?)" }; - } - - return { provider: "megadebrid", username, accountType, daysRemaining, loyaltyPoints }; - } catch (err) { - return { provider: "megadebrid", username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: compactErrorText(err) }; - } - }); - } - public invalidateSession(): void { this.cookie = ""; this.cookieSetAt = 0; diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 751339b..3c435a2 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -4,7 +4,6 @@ import { AppSettings, DuplicatePolicy, HistoryEntry, - ProviderAccountInfo, SessionStats, StartConflictEntry, StartConflictResolutionResult, @@ -31,6 +30,7 @@ const api: ElectronApi = { ipcRenderer.invoke(IPC_CHANNELS.RESOLVE_START_CONFLICT, packageId, policy), clearAll: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_ALL), start: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.START), + startPackages: (packageIds: string[]): Promise => ipcRenderer.invoke(IPC_CHANNELS.START_PACKAGES, packageIds), stop: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.STOP), togglePause: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_PAUSE), cancelPackage: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.CANCEL_PACKAGE, packageId), @@ -54,7 +54,6 @@ const api: ElectronApi = { getHistory: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_HISTORY), clearHistory: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_HISTORY), removeHistoryEntry: (entryId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId), - checkAccount: (provider: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_ACCOUNT, provider), onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => { const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot); ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 520ecf6..29e97c0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -10,7 +10,6 @@ import type { DuplicatePolicy, HistoryEntry, PackageEntry, - ProviderAccountInfo, StartConflictEntry, UiSnapshot, UpdateCheckResult, @@ -93,15 +92,6 @@ const providerLabels: Record = { realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid" }; -const allProviders: DebridProvider[] = ["realdebrid", "megadebrid", "bestdebrid", "alldebrid"]; - -const providerCredentialFields: Record = { - realdebrid: [{ key: "token", label: "API Token", type: "password" }], - megadebrid: [{ key: "megaLogin", label: "Login", type: "text" }, { key: "megaPassword", label: "Passwort", type: "password" }], - bestdebrid: [{ key: "bestToken", label: "API Token", type: "password" }], - alldebrid: [{ key: "allDebridToken", label: "API Key", type: "password" }] -}; - function extractHoster(url: string): string { try { const host = new URL(url).hostname.replace(/^www\./, ""); @@ -452,11 +442,6 @@ export function App(): ReactElement { const latestStateRef = useRef(null); const stateFlushTimerRef = useRef | null>(null); const toastTimerRef = useRef | null>(null); - const [accountInfoMap, setAccountInfoMap] = useState>({}); - const [accountCheckLoading, setAccountCheckLoading] = useState>(new Set()); - const [showAddAccountModal, setShowAddAccountModal] = useState(false); - const [addAccountProvider, setAddAccountProvider] = useState(""); - const [addAccountFields, setAddAccountFields] = useState>({}); const [dragOver, setDragOver] = useState(false); const [editingPackageId, setEditingPackageId] = useState(null); const [editingName, setEditingName] = useState(""); @@ -844,8 +829,6 @@ export function App(): ReactElement { return list; }, [settingsDraft.token, settingsDraft.megaLogin, settingsDraft.megaPassword, settingsDraft.bestToken, settingsDraft.allDebridToken]); - const unconfiguredProviders = useMemo(() => allProviders.filter((p) => !configuredProviders.includes(p)), [configuredProviders]); - const primaryProviderValue: DebridProvider = useMemo(() => { if (configuredProviders.includes(settingsDraft.providerPrimary)) { return settingsDraft.providerPrimary; @@ -1240,42 +1223,6 @@ export function App(): ReactElement { setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: Math.floor(mbps * 1024) })); }; - const checkSingleAccount = (provider: DebridProvider): void => { - setAccountCheckLoading((prev) => new Set(prev).add(provider)); - window.rd.checkAccount(provider).then((info) => { - setAccountInfoMap((prev) => ({ ...prev, [provider]: info })); - }).catch(() => { - setAccountInfoMap((prev) => ({ ...prev, [provider]: { provider, username: "", accountType: "", daysRemaining: null, loyaltyPoints: null, error: "Verbindungsfehler" } })); - }).finally(() => { - setAccountCheckLoading((prev) => { const next = new Set(prev); next.delete(provider); return next; }); - }); - }; - - const checkAllAccounts = (): void => { - for (const provider of configuredProviders) { - checkSingleAccount(provider); - } - }; - - const removeAccount = (provider: DebridProvider): void => { - for (const field of providerCredentialFields[provider]) { - setText(field.key, ""); - } - setAccountInfoMap((prev) => { const next = { ...prev }; delete next[provider]; return next; }); - }; - - const saveAddAccountModal = (): void => { - if (!addAccountProvider) { - return; - } - for (const field of providerCredentialFields[addAccountProvider]) { - setText(field.key, addAccountFields[field.key] || ""); - } - setShowAddAccountModal(false); - setAddAccountProvider(""); - setAddAccountFields({}); - }; - const performQuickAction = async ( action: () => Promise, onError?: (error: unknown) => void @@ -2437,62 +2384,19 @@ export function App(): ReactElement { {settingsSubTab === "accounts" && (

Accounts

- {configuredProviders.length === 0 ? ( -
Keine Accounts konfiguriert
- ) : ( - - - - - - - - - - - - {configuredProviders.map((provider) => { - const info = accountInfoMap[provider]; - const loading = accountCheckLoading.has(provider); - let statusClass = "account-status-unknown"; - let statusText = "Nicht geprüft"; - if (loading) { - statusClass = "account-status-loading"; - statusText = "Prüfe…"; - } else if (info?.error) { - statusClass = "account-status-error"; - statusText = "Fehler"; - } else if (info && (info.accountType === "Premium" || info.accountType === "premium")) { - statusClass = "account-status-ok"; - statusText = "Premium"; - } else if (info && info.accountType) { - statusClass = "account-status-configured"; - statusText = info.accountType; - } - return ( - - - - - - - - ); - })} - -
HosterStatusBenutzernameVerfallsdatumAktionen
{providerLabels[provider]}{statusText}{info?.error ? {info.error} : null}{info?.username || "—"}{info?.daysRemaining !== null && info?.daysRemaining !== undefined ? `${info.daysRemaining} Tage` : "—"} - - -
+ + setText("token", e.target.value)} /> + + setText("megaLogin", e.target.value)} /> + + setText("megaPassword", e.target.value)} /> + + setText("bestToken", e.target.value)} /> + + setText("allDebridToken", e.target.value)} /> + {configuredProviders.length === 0 && ( +
Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.
)} -
- - -
{configuredProviders.length >= 1 && (
{ setAddAccountProvider(e.target.value as DebridProvider); setAddAccountFields({}); }}> - {unconfiguredProviders.map((p) => ())} - -
- {addAccountProvider && providerCredentialFields[addAccountProvider].map((field) => ( -
- - setAddAccountFields((prev) => ({ ...prev, [field.key]: e.target.value }))} /> -
- ))} -
- - -
-
- - )} - {confirmPrompt && (
closeConfirmPrompt(false)}>
event.stopPropagation()}> @@ -2764,8 +2644,8 @@ export function App(): ReactElement {
e.stopPropagation()}> {(!contextMenu.itemId || multi) && hasPackages && ( )} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 1cc3653..7359290 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -1465,88 +1465,6 @@ td { } } -.account-info { - font-size: 13px; - padding: 6px 10px; - border-radius: 8px; - border: 1px solid var(--border); - background: var(--field); -} - -.account-info-success { - color: #4ade80; - border-color: rgba(74, 222, 128, 0.35); -} - -.account-info-error { - color: var(--danger); - border-color: rgba(244, 63, 94, 0.35); -} - -.account-table { - table-layout: fixed; - margin-top: 4px; -} - -.account-table th:first-child, -.account-table td:first-child { - width: 22%; -} - -.account-col-hoster { - font-weight: 600; -} - -.account-status { - display: inline-block; - padding: 2px 8px; - border-radius: 6px; - font-size: 12px; - font-weight: 600; - line-height: 1.4; -} - -.account-status-ok { - background: rgba(74, 222, 128, 0.15); - color: #4ade80; -} - -.account-status-configured { - background: rgba(245, 158, 11, 0.15); - color: #f59e0b; -} - -.account-status-error { - background: rgba(244, 63, 94, 0.12); - color: var(--danger); -} - -.account-status-loading { - background: color-mix(in srgb, var(--accent) 15%, transparent); - color: var(--accent); -} - -.account-status-unknown { - background: color-mix(in srgb, var(--muted) 15%, transparent); - color: var(--muted); -} - -.account-toolbar { - display: flex; - gap: 8px; - margin-top: 4px; -} - -.account-actions-cell { - display: flex; - gap: 6px; -} - -.account-actions-cell .btn { - padding: 4px 8px; - font-size: 12px; -} - .schedule-row { display: grid; grid-template-columns: 56px auto 56px auto 92px auto auto auto; diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index f22b202..edaf39c 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -12,6 +12,7 @@ export const IPC_CHANNELS = { RESOLVE_START_CONFLICT: "queue:resolve-start-conflict", CLEAR_ALL: "queue:clear-all", START: "queue:start", + START_PACKAGES: "queue:start-packages", STOP: "queue:stop", TOGGLE_PAUSE: "queue:toggle-pause", CANCEL_PACKAGE: "queue:cancel-package", @@ -36,6 +37,5 @@ export const IPC_CHANNELS = { EXTRACT_NOW: "queue:extract-now", GET_HISTORY: "history:get", CLEAR_HISTORY: "history:clear", - REMOVE_HISTORY_ENTRY: "history:remove-entry", - CHECK_ACCOUNT: "app:check-account" + REMOVE_HISTORY_ENTRY: "history:remove-entry" } as const; diff --git a/src/shared/preload-api.ts b/src/shared/preload-api.ts index 83241ba..ef8d8d2 100644 --- a/src/shared/preload-api.ts +++ b/src/shared/preload-api.ts @@ -3,7 +3,6 @@ import type { AppSettings, DuplicatePolicy, HistoryEntry, - ProviderAccountInfo, SessionStats, StartConflictEntry, StartConflictResolutionResult, @@ -26,6 +25,7 @@ export interface ElectronApi { resolveStartConflict: (packageId: string, policy: DuplicatePolicy) => Promise; clearAll: () => Promise; start: () => Promise; + startPackages: (packageIds: string[]) => Promise; stop: () => Promise; togglePause: () => Promise; cancelPackage: (packageId: string) => Promise; @@ -49,7 +49,6 @@ export interface ElectronApi { getHistory: () => Promise; clearHistory: () => Promise; removeHistoryEntry: (entryId: string) => Promise; - checkAccount: (provider: string) => Promise; onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void; onClipboardDetected: (callback: (links: string[]) => void) => () => void; onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void; diff --git a/src/shared/types.ts b/src/shared/types.ts index 8e776aa..9093cdc 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -269,15 +269,6 @@ export interface HistoryEntry { outputDir: string; } -export interface ProviderAccountInfo { - provider: DebridProvider; - username: string; - accountType: string; - daysRemaining: number | null; - loyaltyPoints: number | null; - error?: string; -} - export interface HistoryState { entries: HistoryEntry[]; maxEntries: number; diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index ccda779..cfb45f7 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -3636,242 +3636,6 @@ describe("download manager", () => { expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpacken abgebrochen (wird fortgesetzt)"); }); - it("does not recover partial archive files as completed", async () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); - tempDirs.push(root); - - const outputDir = path.join(root, "downloads", "partial-recovery"); - const extractDir = path.join(root, "extract", "partial-recovery"); - fs.mkdirSync(outputDir, { recursive: true }); - - const archivePath = path.join(outputDir, "partial.repack.part1.rar"); - const totalBytes = 1_000_000; - fs.writeFileSync(archivePath, Buffer.alloc(860_000, 1)); - - const session = emptySession(); - const packageId = "partial-recovery-pkg"; - const itemId = "partial-recovery-item"; - const createdAt = Date.now() - 20_000; - session.packageOrder = [packageId]; - session.packages[packageId] = { - id: packageId, - name: "partial-recovery", - outputDir, - extractDir, - status: "downloading", - itemIds: [itemId], - cancelled: false, - enabled: true, - createdAt, - updatedAt: createdAt - }; - session.items[itemId] = { - id: itemId, - packageId, - url: "https://dummy/partial-recovery", - provider: "megadebrid", - status: "queued", - retries: 0, - speedBps: 0, - downloadedBytes: 0, - totalBytes, - progressPercent: 0, - fileName: path.basename(archivePath), - targetPath: archivePath, - resumable: true, - attempts: 0, - lastError: "", - fullStatus: "Wartet", - createdAt, - updatedAt: createdAt - }; - - const manager = new DownloadManager( - { - ...defaultSettings(), - token: "rd-token", - outputDir: path.join(root, "downloads"), - extractDir: path.join(root, "extract"), - autoExtract: false - }, - session, - createStoragePaths(path.join(root, "state")) - ); - - const internal = manager as unknown as { - handlePackagePostProcessing: (packageId: string) => Promise; - }; - await internal.handlePackagePostProcessing(packageId); - - const item = manager.getSnapshot().session.items[itemId]; - expect(item?.status).toBe("queued"); - expect(item?.fullStatus).toBe("Wartet"); - }); - - it("recovers near-complete archive files with known size", async () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); - tempDirs.push(root); - - const outputDir = path.join(root, "downloads", "near-complete-recovery"); - const extractDir = path.join(root, "extract", "near-complete-recovery"); - fs.mkdirSync(outputDir, { recursive: true }); - - const archivePath = path.join(outputDir, "near.complete.part1.rar"); - const totalBytes = 1_000_000; - const fileSize = 996_000; - fs.writeFileSync(archivePath, Buffer.alloc(fileSize, 2)); - - const session = emptySession(); - const packageId = "near-complete-recovery-pkg"; - const itemId = "near-complete-recovery-item"; - const createdAt = Date.now() - 20_000; - session.packageOrder = [packageId]; - session.packages[packageId] = { - id: packageId, - name: "near-complete-recovery", - outputDir, - extractDir, - status: "downloading", - itemIds: [itemId], - cancelled: false, - enabled: true, - createdAt, - updatedAt: createdAt - }; - session.items[itemId] = { - id: itemId, - packageId, - url: "https://dummy/near-complete-recovery", - provider: "megadebrid", - status: "queued", - retries: 0, - speedBps: 0, - downloadedBytes: 0, - totalBytes, - progressPercent: 0, - fileName: path.basename(archivePath), - targetPath: archivePath, - resumable: true, - attempts: 0, - lastError: "", - fullStatus: "Wartet", - createdAt, - updatedAt: createdAt - }; - - const manager = new DownloadManager( - { - ...defaultSettings(), - token: "rd-token", - outputDir: path.join(root, "downloads"), - extractDir: path.join(root, "extract"), - autoExtract: false - }, - session, - createStoragePaths(path.join(root, "state")) - ); - - const internal = manager as unknown as { - handlePackagePostProcessing: (packageId: string) => Promise; - }; - await internal.handlePackagePostProcessing(packageId); - - const item = manager.getSnapshot().session.items[itemId]; - expect(item?.status).toBe("completed"); - expect(item?.downloadedBytes).toBe(fileSize); - }); - - it("skips hybrid-ready multipart archives when a completed part is still too small", async () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); - tempDirs.push(root); - - const outputDir = path.join(root, "downloads", "hybrid-size-guard"); - const extractDir = path.join(root, "extract", "hybrid-size-guard"); - fs.mkdirSync(outputDir, { recursive: true }); - - const part1 = path.join(outputDir, "show.s01e01.part1.rar"); - const part2 = path.join(outputDir, "show.s01e01.part2.rar"); - fs.writeFileSync(part1, Buffer.alloc(900_000, 3)); - fs.writeFileSync(part2, Buffer.alloc(700_000, 4)); - - const session = emptySession(); - const packageId = "hybrid-size-guard-pkg"; - const createdAt = Date.now() - 20_000; - session.packageOrder = [packageId]; - session.packages[packageId] = { - id: packageId, - name: "hybrid-size-guard", - outputDir, - extractDir, - status: "downloading", - itemIds: ["hybrid-size-guard-item-1", "hybrid-size-guard-item-2"], - cancelled: false, - enabled: true, - createdAt, - updatedAt: createdAt - }; - session.items["hybrid-size-guard-item-1"] = { - id: "hybrid-size-guard-item-1", - packageId, - url: "https://dummy/hybrid-size-guard/1", - provider: "megadebrid", - status: "completed", - retries: 0, - speedBps: 0, - downloadedBytes: 900_000, - totalBytes: 1_000_000, - progressPercent: 100, - fileName: path.basename(part1), - targetPath: part1, - resumable: true, - attempts: 1, - lastError: "", - fullStatus: "Entpacken - Ausstehend", - createdAt, - updatedAt: createdAt - }; - session.items["hybrid-size-guard-item-2"] = { - id: "hybrid-size-guard-item-2", - packageId, - url: "https://dummy/hybrid-size-guard/2", - provider: "megadebrid", - status: "completed", - retries: 0, - speedBps: 0, - downloadedBytes: 700_000, - totalBytes: 700_000, - progressPercent: 100, - fileName: path.basename(part2), - targetPath: part2, - resumable: true, - attempts: 1, - lastError: "", - fullStatus: "Entpacken - Ausstehend", - createdAt, - updatedAt: createdAt - }; - - const manager = new DownloadManager( - { - ...defaultSettings(), - token: "rd-token", - outputDir: path.join(root, "downloads"), - extractDir: path.join(root, "extract"), - autoExtract: true, - hybridExtract: true - }, - session, - createStoragePaths(path.join(root, "state")) - ); - - const internal = manager as unknown as { - session: ReturnType; - findReadyArchiveSets: (pkg: ReturnType["packages"][string]) => Promise>; - }; - const ready = await internal.findReadyArchiveSets(internal.session.packages[packageId]); - expect(ready.size).toBe(0); - }); - it("recovers pending extraction on startup for completed package", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); tempDirs.push(root);